From 07101b191e5a1ec48ddedb7a2ae1ed5516018dc2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 25 May 2026 02:01:35 +0100 Subject: [PATCH] refactor: remove pi runtime internals Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code. --- ...gent-runtime-boundary-critical-quality.yml | 2 +- ...rocess-tool-boundary-critical-security.yml | 16 +- .github/workflows/codeql-critical-quality.yml | 4 +- .github/workflows/openclaw-release-checks.yml | 6 +- .../workflows/qa-live-transports-convex.yml | 2 +- CHANGELOG.md | 1 + LICENSE | 3 + README.md | 4 +- THIRD_PARTY_NOTICES.md | 37 + .../Sources/OpenClaw/ModelCatalogLoader.swift | 12 +- .../.generated/plugin-sdk-api-baseline.sha256 | 4 +- docs/agent-runtime-architecture.md | 48 + docs/auth-credential-semantics.md | 2 +- docs/channels/group-messages.md | 2 +- docs/cli/mcp.md | 8 +- docs/cli/migrate.md | 2 +- docs/cli/models.md | 4 +- docs/cli/status.md | 2 +- docs/concepts/agent-loop.md | 20 +- docs/concepts/agent-runtimes.md | 44 +- docs/concepts/agent.md | 6 +- docs/concepts/compaction.md | 2 +- docs/concepts/context-engine.md | 10 +- docs/concepts/model-failover.md | 2 +- docs/concepts/model-providers.md | 21 +- docs/concepts/models.md | 4 +- docs/concepts/oauth.md | 2 +- docs/concepts/qa-e2e-automation.md | 2 +- docs/concepts/queue-steering.md | 20 +- docs/concepts/queue.md | 4 +- docs/concepts/system-prompt.md | 2 +- docs/docs.json | 12 +- docs/gateway/background-process.md | 6 +- docs/gateway/config-agents.md | 10 +- docs/gateway/config-tools.md | 4 +- docs/gateway/configuration-reference.md | 6 +- docs/gateway/doctor.md | 4 +- docs/help/debugging.md | 13 +- .../gpt55-codex-agentic-parity-maintainers.md | 2 +- docs/help/gpt55-codex-agentic-parity.md | 4 +- docs/help/testing-live.md | 4 +- docs/help/testing.md | 14 +- docs/index.md | 6 +- docs/nodes/images.md | 2 +- docs/{pi-dev.md => openclaw-agent-runtime.md} | 46 +- docs/pi.md | 574 +-- docs/plan/codex-context-engine-harness.md | 68 +- docs/plugins/architecture-internals.md | 4 +- docs/plugins/bundles.md | 449 ++- docs/plugins/codex-harness-runtime.md | 10 +- docs/plugins/codex-harness.md | 40 +- docs/plugins/codex-native-plugins.md | 2 +- docs/plugins/manifest.md | 2 +- docs/plugins/sdk-agent-harness.md | 35 +- docs/plugins/sdk-migration.md | 26 +- docs/plugins/sdk-overview.md | 15 +- docs/plugins/sdk-provider-plugins.md | 2 +- docs/plugins/sdk-runtime.md | 2 +- docs/providers/bedrock.md | 2 +- docs/providers/openai.md | 36 +- docs/providers/opencode-go.md | 2 +- docs/reference/AGENTS.default.md | 2 +- docs/reference/code-mode.md | 4 +- .../session-management-compaction.md | 32 +- docs/reference/transcript-hygiene.md | 8 +- docs/tools/acp-agents-setup.md | 3 +- docs/tools/acp-agents.md | 3 +- docs/tools/index.md | 32 +- docs/tools/skills.md | 4 +- docs/tools/slash-commands.md | 4 +- docs/tools/subagents.md | 2 +- docs/tools/thinking.md | 4 +- docs/tools/tokenjuice.md | 2 +- docs/tools/tool-search.md | 26 +- docs/tools/trajectory.md | 2 +- docs/web/webchat.md | 6 +- ...instruction-followthrough-repo-contract.md | 2 +- qa/scenarios/index.md | 4 +- .../models/codex-harness-no-meta-leak.md | 11 +- .../gpt55-thinking-visibility-switch.md | 2 +- .../approval-turn-tool-followthrough.md | 2 +- .../auth-profile-doctor-migration-safety.md | 16 +- ...d => codex-legacy-read-tool-vocabulary.md} | 22 +- .../runtime/compaction-retry-mutating-tool.md | 4 +- ...mpty-response-recovery-replay-safe-read.md | 2 +- .../empty-response-retry-budget-exhausted.md | 2 +- ...easoning-only-no-auto-retry-after-write.md | 2 +- ...easoning-only-recovery-replay-safe-read.md | 2 +- .../runtime/streaming-final-integrity.md | 2 +- qa/scenarios/runtime/tools/apply-patch.md | 4 +- qa/scenarios/runtime/tools/bash.md | 6 +- qa/scenarios/runtime/tools/edit.md | 6 +- qa/scenarios/runtime/tools/exec.md | 4 +- qa/scenarios/runtime/tools/fs-list.md | 4 +- qa/scenarios/runtime/tools/fs-read.md | 6 +- qa/scenarios/runtime/tools/fs-write.md | 6 +- qa/scenarios/runtime/tools/grep.md | 4 +- qa/scenarios/runtime/tools/image-generate.md | 4 +- qa/scenarios/runtime/tools/message-tool.md | 2 +- qa/scenarios/runtime/tools/session-status.md | 4 +- qa/scenarios/runtime/tools/sessions-spawn.md | 4 +- qa/scenarios/runtime/tools/web-fetch.md | 4 +- qa/scenarios/runtime/tools/web-search.md | 4 +- .../security/secret-redaction-tool-logs.md | 2 +- .../medium-game-plan-codex-harness.md | 11 +- ...d => medium-game-plan-openclaw-harness.md} | 37 +- scripts/audit-seams.mjs | 6 +- scripts/bench-model.ts | 4 +- scripts/build-all.mjs | 2 +- scripts/control-ui-i18n.ts | 535 +-- scripts/copy-export-html-templates.ts | 63 +- scripts/dev/channel-message-flows.ts | 2 +- scripts/docker/install-sh-e2e/run.sh | 2 +- ...> agent-bundle-mcp-tools-docker-client.ts} | 30 +- ...er.sh => agent-bundle-mcp-tools-docker.sh} | 18 +- scripts/e2e/lib/fixtures/config.mjs | 2 +- .../e2e/lib/live-plugin-tool/assertions.mjs | 4 +- .../lib/openai-chat-tools/write-config.mjs | 4 +- scripts/e2e/npm-telegram-rtt-config.mjs | 4 +- scripts/e2e/parallels/powershell.ts | 2 +- .../session-runtime-context-docker-client.ts | 4 +- scripts/embedded-run-abort-leak.ts | 2 +- scripts/lib/ci-node-test-plan.mjs | 2 +- scripts/lib/docker-e2e-scenarios.mjs | 14 +- scripts/lib/openclaw-e2e-instance.sh | 1 - scripts/lib/openclaw-test-state.mjs | 1 - scripts/lib/plugin-sdk-entrypoints.json | 13 +- scripts/openclaw-cross-os-release-checks.ts | 2 +- scripts/package-mac-app.sh | 2 +- scripts/perf/issue-78851-model-resolution.ts | 2 +- scripts/perf/rtt-regression-audit.md | 2 +- scripts/test-projects.test-support.mjs | 7 +- ...edentials.ts => agent-auth-credentials.ts} | 36 +- ...y-core.ts => agent-auth-discovery-core.ts} | 16 +- ...agent-auth-discovery.external-cli.test.ts} | 18 +- ...h-discovery.ts => agent-auth-discovery.ts} | 27 +- ...h-json.test.ts => agent-auth-json.test.ts} | 26 +- .../{pi-auth-json.ts => agent-auth-json.ts} | 24 +- ...st.ts => agent-bundle-lsp-runtime.test.ts} | 16 +- ...runtime.ts => agent-bundle-lsp-runtime.ts} | 6 +- ...-bundle-lsp-runtime.windows-spawn.test.ts} | 6 +- ...ize.ts => agent-bundle-mcp-materialize.ts} | 10 +- ...test.ts => agent-bundle-mcp-names.test.ts} | 4 +- ...mcp-names.ts => agent-bundle-mcp-names.ts} | 0 ...st.ts => agent-bundle-mcp-runtime.test.ts} | 14 +- ...runtime.ts => agent-bundle-mcp-runtime.ts} | 16 +- ...ss.ts => agent-bundle-mcp-test-harness.ts} | 2 +- ...gent-bundle-mcp-tools.materialize.test.ts} | 8 +- ...bundle-mcp-tools.request-boundary.test.ts} | 8 +- ...mcp-tools.ts => agent-bundle-mcp-tools.ts} | 6 +- ...mcp-types.ts => agent-bundle-mcp-types.ts} | 0 src/agents/agent-command.ts | 4 +- ...tants.ts => agent-compaction-constants.ts} | 0 .../compaction-instructions.test.ts | 0 .../compaction-instructions.ts | 0 .../compaction-safeguard-quality.ts | 0 .../compaction-safeguard-runtime.ts | 4 +- .../compaction-safeguard.test.ts | 10 +- .../compaction-safeguard.ts | 8 +- .../context-pruning.test.ts | 6 +- .../context-pruning.ts | 2 +- .../context-pruning/extension.ts | 2 +- .../context-pruning/pruner.test.ts | 4 +- .../context-pruning/pruner.ts | 8 +- .../context-pruning/runtime.ts | 2 +- .../context-pruning/settings.ts | 0 .../context-pruning/tools.ts | 0 .../session-manager-runtime-registry.ts | 0 ....ts => agent-mcp-style.cache.live.test.ts} | 2 +- ....ts => agent-model-discovery.auth.test.ts} | 28 +- .../agent-model-discovery.internal.test.ts | 11 + ...nt-model-discovery.synthetic-auth.test.ts} | 16 +- ....test.ts => agent-model-discovery.test.ts} | 6 +- src/agents/agent-model-discovery.ts | 173 + ....ts => agent-project-settings-snapshot.ts} | 70 +- ... => agent-project-settings.bundle.test.ts} | 33 +- ...test.ts => agent-project-settings.test.ts} | 66 +- ...-settings.ts => agent-project-settings.ts} | 36 +- src/agents/agent-runtime-id.ts | 42 + src/agents/agent-scope-config.ts | 8 +- src/agents/agent-scope.ts | 7 +- ...ettings.test.ts => agent-settings.test.ts} | 126 +- .../{pi-settings.ts => agent-settings.ts} | 38 +- ...dapter.after-tool-call.fires-once.test.ts} | 16 +- ...efinition-adapter.after-tool-call.test.ts} | 8 +- ...t-tool-definition-adapter.logging.test.ts} | 20 +- ... => agent-tool-definition-adapter.test.ts} | 10 +- ...er.ts => agent-tool-definition-adapter.ts} | 32 +- ... agent-tool-handler-state.test-helpers.ts} | 2 +- ... => agent-tools-agent-config.exec.test.ts} | 2 +- ...st.ts => agent-tools-agent-config.test.ts} | 6 +- ...ema.ts => agent-tools-parameter-schema.ts} | 0 ...pi-tools.abort.ts => agent-tools.abort.ts} | 4 +- ...st.ts => agent-tools.availability.test.ts} | 2 +- ... agent-tools.before-tool-call.e2e.test.ts} | 2 +- ...ls.before-tool-call.embedded-mode.test.ts} | 2 +- ....before-tool-call.integration.e2e.test.ts} | 14 +- ...> agent-tools.before-tool-call.runtime.ts} | 0 ... => agent-tools.before-tool-call.state.ts} | 0 ...all.ts => agent-tools.before-tool-call.ts} | 4 +- ...liases-schemas-without-dropping-g.test.ts} | 4 +- ...ools.create-openclaw-coding-tools.test.ts} | 22 +- ...test.ts => agent-tools.cron-scope.test.ts} | 2 +- ...-tools.deferred-followup-guidance.test.ts} | 4 +- ...up.ts => agent-tools.deferred-followup.ts} | 2 +- ....host-edit.ts => agent-tools.host-edit.ts} | 8 +- ...ent-tools.message-provider-policy.test.ts} | 2 +- ...=> agent-tools.message-provider-policy.ts} | 0 ...nt-tools.model-provider-collision.test.ts} | 4 +- ...ams.test.ts => agent-tools.params.test.ts} | 2 +- ...-tools.params.ts => agent-tools.params.ts} | 2 +- ...icy.test.ts => agent-tools.policy.test.ts} | 12 +- ...-tools.policy.ts => agent-tools.policy.ts} | 2 +- ...agent-tools.read.host-edit-access.test.ts} | 8 +- ...ent-tools.read.host-edit-recovery.test.ts} | 6 +- ...t-tools.read.host-tilde-expansion.test.ts} | 8 +- .../{pi-tools.read.ts => agent-tools.read.ts} | 18 +- ...t-tools.read.workspace-root-guard.test.ts} | 6 +- ....test.ts => agent-tools.safe-bins.test.ts} | 6 +- ...dbox-mounted-paths.workspace-only.test.ts} | 6 +- ...ema.test.ts => agent-tools.schema.test.ts} | 10 +- ...-tools.schema.ts => agent-tools.schema.ts} | 6 +- src/agents/{pi-tools.ts => agent-tools.ts} | 52 +- ...pi-tools.types.ts => agent-tools.types.ts} | 0 ... agent-tools.workspace-only-false.test.ts} | 14 +- ...ts => agent-tools.workspace-paths.test.ts} | 8 +- src/agents/anthropic-payload-log.test.ts | 2 +- src/agents/anthropic-payload-log.ts | 6 +- src/agents/anthropic-payload-policy.test.ts | 8 +- src/agents/anthropic-payload-policy.ts | 2 +- .../anthropic-transport-stream.live.test.ts | 2 +- src/agents/anthropic-transport-stream.test.ts | 2 +- src/agents/anthropic-transport-stream.ts | 4 +- src/agents/anthropic-vertex-stream.ts | 2 +- src/agents/anthropic.setup-token.live.test.ts | 12 +- src/agents/apply-patch.ts | 2 +- .../auth-profile-runtime-contract.test.ts | 36 +- ...th-profiles.ensureauthprofilestore.test.ts | 39 +- src/agents/auth-profiles.store-cache.test.ts | 7 - .../auth-profiles/oauth-manager.test.ts | 3 - .../auth-profiles/oauth-refresh-queue.test.ts | 2 +- src/agents/auth-profiles/oauth-test-utils.ts | 7 +- .../oauth.adopt-identity.test.ts | 2 +- .../oauth.concurrent-agents.test.ts | 2 +- .../oauth.fallback-to-main-agent.test.ts | 11 +- .../oauth.mirror-refresh.test.ts | 2 +- ...auth.openai-codex-refresh-fallback.test.ts | 7 +- src/agents/auth-profiles/oauth.ts | 2 +- src/agents/auth-profiles/order.ts | 5 +- src/agents/bash-process-registry.ts | 2 +- .../bash-tools.exec-approval-followup.ts | 2 +- ...sh-tools.exec-gateway-approval.e2e.test.ts | 2 +- src/agents/bash-tools.exec-host-gateway.ts | 2 +- .../bash-tools.exec-host-node-phases.ts | 2 +- src/agents/bash-tools.exec-host-node.ts | 2 +- src/agents/bash-tools.exec-host-shared.ts | 2 +- src/agents/bash-tools.exec-runtime.ts | 6 +- src/agents/bash-tools.exec-types.ts | 2 +- src/agents/bash-tools.exec.ts | 6 +- src/agents/bash-tools.process-send-keys.ts | 2 +- src/agents/bash-tools.process.ts | 2 +- src/agents/bootstrap-budget.ts | 23 +- src/agents/bootstrap-files.ts | 6 +- src/agents/btw-transcript.ts | 32 +- src/agents/btw.test.ts | 14 +- src/agents/btw.ts | 16 +- src/agents/bundle-mcp-config.ts | 2 +- src/agents/cache-trace.ts | 2 +- src/agents/chutes-oauth.ts | 2 +- src/agents/cli-runner.context-engine.test.ts | 2 +- src/agents/cli-runner.helpers.test.ts | 4 +- src/agents/cli-runner.reliability.test.ts | 2 +- src/agents/cli-runner.test-support.ts | 2 +- src/agents/cli-runner.ts | 18 +- src/agents/cli-runner/claude-live-session.ts | 230 +- src/agents/cli-runner/execute.ts | 2 +- .../cli-runner/helpers.system-prompt.test.ts | 10 +- src/agents/cli-runner/helpers.ts | 8 +- src/agents/cli-runner/prepare.test.ts | 4 +- src/agents/cli-runner/prepare.ts | 12 +- src/agents/cli-runner/reliability.ts | 2 +- src/agents/cli-runner/session-history.test.ts | 2 +- src/agents/cli-runner/session-history.ts | 4 +- src/agents/cli-runner/types.ts | 6 +- src/agents/code-mode.ts | 18 +- .../codex-app-server.extensions.test.ts | 10 +- src/agents/codex-mcp-config.test.ts | 2 +- src/agents/codex-mcp-config.ts | 2 +- src/agents/command/attempt-callbacks.ts | 2 +- .../command/attempt-execution.cli.test.ts | 132 +- src/agents/command/attempt-execution.ts | 30 +- src/agents/command/cli-compaction.test.ts | 483 ++- src/agents/command/cli-compaction.ts | 133 +- src/agents/command/delivery.ts | 12 +- src/agents/command/session-store.test.ts | 34 +- src/agents/command/session-store.ts | 2 +- src/agents/compaction-partial-summary.test.ts | 10 +- src/agents/compaction-real-conversation.ts | 2 +- ...compaction.identifier-preservation.test.ts | 12 +- src/agents/compaction.retry.test.ts | 14 +- .../compaction.summarize-fallback.test.ts | 32 +- src/agents/compaction.test.ts | 6 +- src/agents/compaction.token-sanitize.test.ts | 16 +- .../compaction.tool-result-details.test.ts | 34 +- src/agents/compaction.ts | 13 +- src/agents/config.ts | 564 +++ src/agents/context.lookup.test.ts | 4 +- src/agents/context.test.ts | 2 +- src/agents/context.ts | 9 +- src/agents/copilot-dynamic-headers.ts | 2 +- src/agents/custom-api-registry.test.ts | 26 +- src/agents/custom-api-registry.ts | 4 +- ...s => embedded-agent-block-chunker.test.ts} | 2 +- ...ker.ts => embedded-agent-block-chunker.ts} | 0 ... embedded-agent-error-observation.test.ts} | 2 +- ...ts => embedded-agent-error-observation.ts} | 2 +- ...elpers.buildbootstrapcontextfiles.test.ts} | 2 +- ...-helpers.formatassistanterrortext.test.ts} | 6 +- ...ent-helpers.isbillingerrormessage.test.ts} | 12 +- ...sistant-text-blocks-but-preserves.test.ts} | 6 +- ...nt-helpers.sanitizeuserfacingtext.test.ts} | 12 +- ...d-helpers.ts => embedded-agent-helpers.ts} | 22 +- ...dded-agent-helpers.validate-turns.test.ts} | 4 +- .../bootstrap.test.ts | 0 .../bootstrap.ts | 2 +- .../errors.test.ts | 2 +- .../errors.ts | 6 +- .../failover-matches.test.ts | 0 .../failover-matches.ts | 2 +- .../google.ts | 0 .../images.ts | 2 +- .../messaging-dedupe.ts | 0 .../openai.ts | 4 +- .../provider-error-patterns.test.ts | 0 .../provider-error-patterns.ts | 0 .../sanitize-user-facing-text.ts | 0 .../thinking.test.ts | 0 .../thinking.ts | 0 .../turns.ts | 2 +- .../types.ts | 0 ...bedded-pi-lsp.ts => embedded-agent-lsp.ts} | 6 +- ...bedded-pi-mcp.ts => embedded-agent-mcp.ts} | 6 +- ...ssaging.ts => embedded-agent-messaging.ts} | 0 ...s.ts => embedded-agent-messaging.types.ts} | 0 ...payloads.ts => embedded-agent-payloads.ts} | 0 ...agent-runner-extraparams-moonshot.test.ts} | 6 +- ...ent-runner-extraparams-openrouter.test.ts} | 14 +- ...-agent-runner-extraparams-resolve.test.ts} | 2 +- ...ded-agent-runner-extraparams.live.test.ts} | 10 +- ...-agent-runner-extraparams.test-support.ts} | 6 +- ...embedded-agent-runner-extraparams.test.ts} | 84 +- ...runner.anthropic-tool-replay.live.test.ts} | 8 +- ...t-runner.buildembeddedsandboxinfo.test.ts} | 4 +- ... embedded-agent-runner.cache.live.test.ts} | 12 +- ...-runner.compaction-safety-timeout.test.ts} | 2 +- ...runner.createsystempromptoverride.test.ts} | 2 +- ...t.ts => embedded-agent-runner.e2e.test.ts} | 110 +- ... embedded-agent-runner.extensions.test.ts} | 12 +- ...ts => embedded-agent-runner.guard.test.ts} | 4 +- ...er.guard.waitforidle-before-flush.test.ts} | 6 +- ...ed-agent-runner.limithistoryturns.test.ts} | 4 +- ...unner.openai-tool-id-preservation.test.ts} | 6 +- ...ent-runner.resolvesessionagentids.test.ts} | 0 ...d-agent.auth-profile-rotation.e2e.test.ts} | 66 +- ...r.sanitize-session-history.policy.test.ts} | 4 +- ....sanitize-session-history.test-harness.ts} | 12 +- ...t-runner.sanitize-session-history.test.ts} | 14 +- ...bedded-agent-runner.splitsdktools.test.ts} | 8 +- src/agents/embedded-agent-runner.ts | 25 + .../abort.ts | 0 .../embedded-agent-runner/aliases.test.ts | 26 + .../cache-ttl.test.ts | 0 .../cache-ttl.ts | 8 +- .../compact-reasons.test.ts | 0 .../compact-reasons.ts | 0 .../compact.hooks.harness.ts | 48 +- .../compact.hooks.test.ts | 66 +- .../compact.queued.ts | 48 +- .../embedded-agent-runner/compact.runtime.ts | 15 + .../compact.runtime.types.ts | 6 + .../compact.ts | 147 +- .../compact.types.ts | 2 +- ...compaction-duplicate-user-messages.test.ts | 0 .../compaction-duplicate-user-messages.ts | 0 .../compaction-hooks.ts | 2 +- .../compaction-runtime-context.test.ts | 0 .../compaction-runtime-context.ts | 0 .../compaction-safety-timeout.ts | 0 .../compaction-successor-transcript.test.ts | 2 +- .../compaction-successor-transcript.ts | 4 +- .../context-engine-capabilities.ts | 0 .../context-engine-maintenance.test.ts | 0 .../context-engine-maintenance.ts | 0 .../context-truncation-notice.ts | 0 .../delivery-evidence.ts | 0 .../effective-tool-policy.test.ts | 0 .../effective-tool-policy.ts | 2 +- .../empty-assistant-turn.ts | 2 +- .../execution-phase.ts | 0 .../extensions.test.ts | 14 +- .../extensions.ts | 35 +- ...tra-params.cache-retention-default.test.ts | 8 +- .../extra-params.google.test.ts | 6 +- .../extra-params.kilocode.test.ts | 9 +- ...ra-params.openrouter-cache-control.test.ts | 4 +- .../extra-params.provider-runtime.test.ts | 6 +- .../extra-params.sampling.test.ts | 6 +- .../extra-params.test-support.ts | 4 +- .../extra-params.ts | 38 +- .../extra-params.zai-tool-stream.test.ts | 6 +- .../failure-signal.test.ts | 0 .../failure-signal.ts | 0 .../google-prompt-cache.test.ts | 4 +- .../google-prompt-cache.ts | 8 +- .../history.test.ts | 0 .../history.ts | 2 +- .../kilocode.test.ts | 0 .../lanes.test.ts | 0 .../lanes.ts | 0 .../logger.ts | 0 .../manual-compaction-boundary.test.ts | 6 +- .../manual-compaction-boundary.ts | 4 +- .../message-action-discovery-input.test.ts | 0 .../message-action-discovery-input.ts | 0 .../model-context-tokens.ts | 10 + .../model-discovery-cache.ts | 18 +- ...orward-compat.errors-and-overrides.test.ts | 4 +- .../model.forward-compat.test-support.ts | 0 .../model.forward-compat.test.ts | 0 .../model.inline-provider.test.ts | 0 .../model.inline-provider.ts | 2 +- .../model.provider-normalization.ts | 6 + .../model.provider-runtime.test-support.ts | 0 .../model.skip-agent-discovery-hooks.test.ts} | 12 +- .../model.startup-retry.test.ts | 2 +- .../model.static-catalog.test.ts | 0 .../model.static-catalog.ts | 8 +- .../model.test-harness.ts | 2 +- .../model.test.ts | 36 +- .../model.ts | 112 +- .../openrouter-model-capabilities.test.ts | 0 .../openrouter-model-capabilities.ts | 0 .../post-compaction-loop-guard.test.ts | 0 .../post-compaction-loop-guard.ts | 0 .../prompt-cache-observability.test.ts | 0 .../prompt-cache-observability.ts | 0 .../prompt-cache-retention.test.ts | 0 .../prompt-cache-retention.ts | 2 +- .../replay-history.test.ts | 2 +- .../replay-history.ts | 13 +- .../replay-state.ts | 0 .../resource-loader.test.ts | 16 +- .../resource-loader.ts | 8 +- .../result-fallback-classifier.test.ts | 6 +- .../result-fallback-classifier.ts | 12 +- .../run-state.ts | 10 +- .../run.before-agent-reply-cron.test.ts | 16 +- .../run.codex-app-server-recovery.test.ts | 20 +- .../run.codex-server-error-fallback.test.ts | 8 +- .../run.compaction-loop-guard.test.ts | 22 +- ...ss-provider-fallback-error-context.test.ts | 10 +- .../run.empty-error-retry.test.ts | 20 +- .../run.incomplete-turn.test.ts | 68 +- .../run.overflow-compaction.fixture.ts | 0 .../run.overflow-compaction.harness.ts | 10 +- .../run.overflow-compaction.loop.test.ts | 52 +- .../run.overflow-compaction.test.ts | 70 +- .../run.timeout-triggered-compaction.test.ts | 36 +- .../run.ts | 104 +- .../run/AGENTS.md | 0 .../run/CLAUDE.md | 0 .../run/abortable.test.ts | 0 .../run/abortable.ts | 0 .../run/assistant-failover.test.ts | 2 +- .../run/assistant-failover.ts | 6 +- .../run/attempt-bootstrap-routing.ts | 0 .../run/attempt-http-runtime.ts | 0 .../run/attempt-session.ts | 2 +- .../run/attempt-stage-timing.test.ts | 0 .../run/attempt-stage-timing.ts | 0 .../run/attempt-system-prompt.test.ts | 0 .../run/attempt-system-prompt.ts | 0 .../attempt-tool-construction-plan.test.ts | 32 + .../run/attempt-tool-construction-plan.ts | 4 +- .../run/attempt-trajectory-status.test.ts | 0 .../run/attempt-trajectory-status.ts | 0 .../run/attempt.context-engine-helpers.ts | 4 +- .../attempt.memory-flush-forwarding.test.ts | 10 +- .../attempt.model-diagnostic-events.test.ts | 2 +- .../run/attempt.model-diagnostic-events.ts | 2 +- .../run/attempt.prompt-helpers.test.ts | 0 .../run/attempt.prompt-helpers.ts | 7 +- .../run/attempt.queue-message.test.ts | 18 +- .../run/attempt.session-lock.test.ts | 31 +- .../run/attempt.session-lock.ts | 28 +- .../run/attempt.sessions-yield.ts | 6 +- ...t.spawn-workspace.bootstrap-marker.test.ts | 0 ....spawn-workspace.bootstrap-routing.test.ts | 0 ....spawn-workspace.bootstrap-warning.test.ts | 0 .../attempt.spawn-workspace.cache-ttl.test.ts | 0 ...mpt.spawn-workspace.context-engine.test.ts | 133 +- ....spawn-workspace.context-injection.test.ts | 2 +- ...pt.spawn-workspace.resource-loader.test.ts | 0 ...mpt.spawn-workspace.sessions-spawn.test.ts | 6 +- .../attempt.spawn-workspace.test-support.ts | 104 +- .../attempt.spawn-workspace.timeout.test.ts | 0 .../run/attempt.stop-reason-recovery.test.ts | 8 +- .../run/attempt.stop-reason-recovery.ts | 4 +- .../run/attempt.subscription-cleanup.test.ts | 0 .../run/attempt.subscription-cleanup.ts | 6 +- .../run/attempt.test.ts | 2 +- .../run/attempt.thread-helpers.ts | 0 .../attempt.tool-call-argument-repair.test.ts | 0 .../run/attempt.tool-call-argument-repair.ts | 4 +- .../attempt.tool-call-normalization.test.ts | 2 +- .../run/attempt.tool-call-normalization.ts | 12 +- .../run/attempt.tool-run-context.ts | 0 .../run/attempt.transcript-policy.test.ts | 0 .../run/attempt.transcript-policy.ts | 0 .../run/attempt.ts | 301 +- .../run/auth-controller.test.ts | 10 +- .../run/auth-controller.ts | 32 +- .../run/auth-profile-failure-policy.test.ts | 0 .../run/auth-profile-failure-policy.ts | 2 +- .../run/auth-profile-failure-policy.types.ts | 0 .../run/backend.test.ts | 10 +- .../run/backend.ts | 0 .../run/codex-app-server-recovery.ts | 0 ...compaction-retry-aggregate-timeout.test.ts | 0 .../run/compaction-retry-aggregate-timeout.ts | 0 .../run/compaction-timeout.test.ts | 0 .../run/compaction-timeout.ts | 2 +- .../run/failover-observation.test.ts | 0 .../run/failover-observation.ts | 4 +- .../run/failover-policy.test.ts | 0 .../run/failover-policy.ts | 2 +- .../run/fallbacks.test.ts | 0 .../run/fallbacks.ts | 0 .../run/helpers.resolve-error-context.test.ts | 6 +- .../run/helpers.test.ts | 2 +- .../run/helpers.ts | 12 +- .../run/history-image-prune.test.ts | 4 +- .../run/history-image-prune.ts | 2 +- .../run/idle-timeout-breaker.test.ts | 0 .../run/idle-timeout-breaker.ts | 0 .../run/images.test.ts | 0 .../run/images.ts | 2 +- .../run/incomplete-turn.ts | 6 +- .../run/llm-idle-timeout.test.ts | 2 +- .../run/llm-idle-timeout.ts | 4 +- .../run/message-merge-strategy.test.ts | 0 .../run/message-merge-strategy.ts | 0 .../run/message-tool-terminal.test.ts | 2 +- .../run/message-tool-terminal.ts | 17 +- .../run/midturn-precheck.ts | 0 .../run/params.ts | 14 +- .../run/payloads.errors.test.ts | 4 +- .../run/payloads.test-helpers.ts | 0 .../run/payloads.test.ts | 2 +- .../run/payloads.ts | 13 +- .../run/preemptive-compaction.test.ts | 4 +- .../run/preemptive-compaction.ts | 6 +- .../run/preemptive-compaction.types.ts | 0 .../run/retry-limit.ts | 6 +- .../run/runtime-context-prompt.test.ts | 0 .../run/runtime-context-prompt.ts | 0 .../run/setup.test.ts | 0 .../run/setup.ts | 6 +- .../run/stream-wrapper.ts | 2 +- .../run/tool-media-payloads.test.ts | 0 .../run/tool-media-payloads.ts | 14 +- ...transcript-repair-runtime-contract.test.ts | 2 +- .../run/trigger-policy.test.ts | 0 .../run/trigger-policy.ts | 0 .../run/types.ts | 16 +- .../runs.test.ts | 58 +- .../runs.ts | 103 +- .../sandbox-info.ts | 0 ...ession-history.tool-result-details.test.ts | 6 +- .../session-file-key.ts | 0 .../session-manager-cache.test.ts | 0 .../session-manager-cache.ts | 0 .../session-manager-init.ts | 2 +- .../sessions-yield.orchestration.test.ts | 18 +- .../skills-runtime.integration.test.ts | 0 .../skills-runtime.test.ts | 0 .../skills-runtime.ts | 0 .../stream-resolution.test.ts | 40 +- .../stream-resolution.ts | 38 +- .../system-prompt.test.ts | 4 +- .../system-prompt.ts | 8 +- .../thinking.test.ts | 4 +- .../thinking.ts | 4 +- .../tool-call-argument-decoding.ts | 4 +- .../tool-name-allowlist.test.ts | 26 +- .../tool-name-allowlist.ts | 11 +- .../tool-result-char-estimator.test.ts | 2 +- .../tool-result-char-estimator.ts | 2 +- .../tool-result-context-guard.test.ts | 18 +- .../tool-result-context-guard.ts | 6 +- .../tool-result-truncation.test.ts | 6 +- .../tool-result-truncation.ts | 8 +- .../tool-schema-runtime.test.ts | 0 .../tool-schema-runtime.ts | 2 +- .../tool-split.ts | 6 +- .../transcript-file-state.test.ts | 0 .../transcript-file-state.ts | 11 +- .../transcript-rewrite.test.ts | 4 +- .../transcript-rewrite.ts | 4 +- .../types.ts | 16 +- .../usage-accumulator.test.ts | 0 .../usage-accumulator.ts | 0 .../usage-reporting.test.ts | 26 +- .../utils.ts | 6 +- .../wait-for-idle-before-flush.ts | 0 ...-subscribe.block-reply-rejections.test.ts} | 4 +- ...ent-subscribe.code-span-awareness.test.ts} | 8 +- ...gent-subscribe.compaction-test-helpers.ts} | 0 ...> embedded-agent-subscribe.e2e-harness.ts} | 32 +- ...-subscribe.handlers.compaction.runtime.ts} | 0 ...ent-subscribe.handlers.compaction.test.ts} | 10 +- ...ed-agent-subscribe.handlers.compaction.ts} | 17 +- ...gent-subscribe.handlers.lifecycle.test.ts} | 14 +- ...ded-agent-subscribe.handlers.lifecycle.ts} | 22 +- ...agent-subscribe.handlers.messages.test.ts} | 10 +- ...dded-agent-subscribe.handlers.messages.ts} | 50 +- ...nt-subscribe.handlers.tools.media.test.ts} | 119 +- ...ed-agent-subscribe.handlers.tools.test.ts} | 10 +- ...mbedded-agent-subscribe.handlers.tools.ts} | 32 +- ...s => embedded-agent-subscribe.handlers.ts} | 20 +- ...mbedded-agent-subscribe.handlers.types.ts} | 34 +- ...subscribe.lifecycle-billing-error.test.ts} | 4 +- ...ubscribe.openai-responses.test-helpers.ts} | 0 ...ts => embedded-agent-subscribe.promise.ts} | 0 ...=> embedded-agent-subscribe.raw-stream.ts} | 0 ...bedded-agent-subscribe.reply-tags.test.ts} | 12 +- ... embedded-agent-subscribe.shared-types.ts} | 2 +- ...ore-tool-execution-start-preserve.test.ts} | 26 +- ...es-not-append-text-end-content-is.test.ts} | 4 +- ...onblockreplyflush-callback-is-not.test.ts} | 8 +- ...t-duplicate-text-end-repeats-full.test.ts} | 4 +- ...emit-duplicate-block-replies-text.test.ts} | 16 +- ...s-block-replies-text-end-does-not.test.ts} | 8 +- ...oning-as-separate-message-enabled.test.ts} | 10 +- ...ppresses-output-without-start-tag.test.ts} | 44 +- ...as-action-metadata-tool-summaries.test.ts} | 4 +- ...ts-final-answer-block-replies-are.test.ts} | 10 +- ...eps-indented-fenced-blocks-intact.test.ts} | 4 +- ...nced-blocks-splitting-inside-them.test.ts} | 4 +- ...-single-line-fenced-blocks-reopen.test.ts} | 20 +- ...-soft-chunks-paragraph-preference.test.ts} | 4 +- ...ion.subscribeembeddedagentsession.test.ts} | 79 +- ...uppresses-commentary-phase-output.test.ts} | 6 +- ...ge-end-block-replies-message-tool.test.ts} | 10 +- ...mpaction-retries-before-resolving.test.ts} | 4 +- ...-agent-subscribe.tool-text-diagnostics.ts} | 6 +- ...ded-agent-subscribe.tools.extract.test.ts} | 2 +- ...edded-agent-subscribe.tools.media.test.ts} | 6 +- ...=> embedded-agent-subscribe.tools.test.ts} | 2 +- ...s.ts => embedded-agent-subscribe.tools.ts} | 6 +- ...bscribe.ts => embedded-agent-subscribe.ts} | 52 +- ...s.ts => embedded-agent-subscribe.types.ts} | 14 +- ...-utils.strip-model-special-tokens.test.ts} | 2 +- ...s.test.ts => embedded-agent-utils.test.ts} | 4 +- ...edded-utils.ts => embedded-agent-utils.ts} | 6 +- src/agents/embedded-agent.runtime.ts | 10 + .../{pi-embedded.ts => embedded-agent.ts} | 19 +- src/agents/execution-contract.test.ts | 6 +- src/agents/execution-contract.ts | 2 +- src/agents/failover-error.test.ts | 4 +- src/agents/failover-error.ts | 8 +- src/agents/failover-policy.test.ts | 2 +- src/agents/failover-policy.ts | 2 +- src/agents/google-gemini-switch.live.test.ts | 2 +- src/agents/harness-runtimes.test.ts | 4 +- src/agents/harness-runtimes.ts | 63 +- src/agents/harness/builtin-openclaw.ts | 13 + src/agents/harness/builtin-pi.ts | 13 - .../harness/codex-app-server-extensions.ts | 2 +- .../harness/context-engine-lifecycle.test.ts | 2 +- .../harness/context-engine-lifecycle.ts | 8 +- src/agents/harness/hook-helpers.ts | 4 +- src/agents/harness/native-hook-relay.ts | 10 +- src/agents/harness/policy.ts | 12 +- .../harness/prompt-compaction-hook-helpers.ts | 2 +- src/agents/harness/registry.test.ts | 16 +- src/agents/harness/runtime-plugin.test.ts | 2 +- src/agents/harness/runtime-plugin.ts | 34 +- src/agents/harness/selection.test.ts | 134 +- src/agents/harness/selection.ts | 83 +- .../harness/tool-result-middleware.test.ts | 14 +- src/agents/harness/tool-result-middleware.ts | 2 +- src/agents/harness/types.ts | 14 +- src/agents/harness/v2.test.ts | 16 +- src/agents/live-cache-regression-runner.ts | 2 +- src/agents/live-cache-test-support.ts | 19 +- src/agents/live-model-switch.test.ts | 24 +- src/agents/live-model-switch.ts | 14 +- src/agents/live-model-turn-probes.ts | 23 +- src/agents/live-test-helpers.ts | 2 +- src/agents/live-test-provider-drift.ts | 6 +- src/agents/local-model-lean.test.ts | 2 +- src/agents/local-model-lean.ts | 2 +- src/agents/minimax.live.test.ts | 2 +- src/agents/model-auth.profiles.test.ts | 6 +- src/agents/model-auth.test.ts | 34 +- src/agents/model-catalog.test.ts | 74 +- src/agents/model-compat.test.ts | 34 +- src/agents/model-fallback-observation.ts | 4 +- .../model-fallback.run-embedded.e2e.test.ts | 22 +- src/agents/model-fallback.test.ts | 28 +- src/agents/model-fallback.ts | 27 +- src/agents/model-fallback.types.ts | 2 +- src/agents/model-registry-loader.ts | 30 + src/agents/model-runtime-aliases.test.ts | 4 +- src/agents/model-runtime-aliases.ts | 2 +- src/agents/model-runtime-policy.test.ts | 22 +- src/agents/model-runtime-policy.ts | 2 +- src/agents/models-config.e2e-harness.ts | 10 +- ...s-writing-models-json-no-env-token.test.ts | 1 - src/agents/models.profiles.live.test.ts | 38 +- .../modes/interactive/components/diff.ts | 158 + .../components/keybinding-hints.ts | 53 + .../interactive/components/visual-truncate.ts | 50 + src/agents/modes/interactive/theme/dark.json | 86 + src/agents/modes/interactive/theme/light.json | 85 + .../modes/interactive/theme/theme-schema.json | 335 ++ src/agents/modes/interactive/theme/theme.ts | 1272 +++++++ src/agents/moonshot.live.test.ts | 2 +- src/agents/openai-codex-routing.test.ts | 109 +- src/agents/openai-codex-routing.ts | 92 +- src/agents/openai-completions-compat.ts | 2 +- .../openai-reasoning-compat.live.test.ts | 16 +- src/agents/openai-reasoning-effort.test.ts | 2 +- .../openai-responses-payload-policy.test.ts | 2 +- .../openai-responses.reasoning-replay.test.ts | 4 +- src/agents/openai-text-verbosity.ts | 2 +- src/agents/openai-thinking-contract.test.ts | 18 +- src/agents/openai-tool-schema.ts | 2 +- src/agents/openai-transport-stream.test.ts | 18 +- src/agents/openai-transport-stream.ts | 84 +- ...enclaw-owned-tool-runtime-contract.test.ts | 44 +- .../openclaw-tools.nodes-workspace-guard.ts | 2 +- ...subagents.sessions-spawn.lifecycle.test.ts | 8 +- src/agents/openclaw-tools.ts | 10 +- src/agents/openclaw-tools.update-plan.test.ts | 18 +- .../outcome-fallback-runtime-contract.test.ts | 13 +- src/agents/pi-embedded-runner.ts | 41 - src/agents/pi-embedded-runner/aliases.test.ts | 17 - .../anthropic-cache-control-payload.test.ts | 35 - .../anthropic-cache-control-payload.ts | 1 - .../anthropic-family-cache-semantics.ts | 106 - .../anthropic-family-tool-payload-compat.ts | 172 - .../pi-embedded-runner/compact.runtime.ts | 15 - .../compact.runtime.types.ts | 6 - .../google-stream-wrappers.test.ts | 149 - .../google-stream-wrappers.ts | 4 - .../minimax-stream-wrappers.test.ts | 124 - .../minimax-stream-wrappers.ts | 78 - .../model-context-tokens.ts | 11 - .../model.provider-normalization.ts | 9 - .../moonshot-stream-wrappers.ts | 32 - .../moonshot-thinking-stream-wrappers.ts | 133 - .../openai-stream-wrappers.test.ts | 585 --- .../openai-stream-wrappers.ts | 704 ---- .../proxy-stream-wrappers.test.ts | 207 -- .../proxy-stream-wrappers.ts | 256 -- .../reasoning-effort-utils.test.ts | 23 - .../reasoning-effort-utils.ts | 16 - src/agents/pi-embedded-runner/runtime.ts | 24 - .../stream-payload-utils.ts | 20 - .../pi-embedded-runner/zai-stream-wrappers.ts | 28 - src/agents/pi-embedded.runtime.ts | 11 - src/agents/pi-model-discovery-runtime.ts | 10 - .../pi-model-discovery.compat.e2e.test.ts | 24 - src/agents/pi-model-discovery.ts | 267 -- src/agents/plugin-text-transforms.test.ts | 4 +- src/agents/plugin-text-transforms.ts | 9 +- src/agents/prompt-surface.ts | 8 +- src/agents/provider-local-service.test.ts | 2 +- src/agents/provider-local-service.ts | 6 +- src/agents/provider-request-config.ts | 2 +- src/agents/provider-transport-fetch.test.ts | 2 +- src/agents/provider-transport-stream.test.ts | 4 +- src/agents/realtime-bootstrap-context.ts | 2 +- src/agents/run-cleanup-timeout.test.ts | 26 +- src/agents/run-cleanup-timeout.ts | 2 +- src/agents/runtime-plan/auth.ts | 12 +- src/agents/runtime-plan/build.test.ts | 6 +- src/agents/runtime-plan/build.ts | 10 +- .../runtime-plan/tools.diagnostics.test.ts | 2 +- src/agents/runtime-plan/tools.test.ts | 4 +- src/agents/runtime-plan/tools.ts | 4 +- src/agents/runtime-plan/types.compat.test.ts | 2 +- src/agents/runtime-plan/types.test.ts | 2 +- src/agents/runtime-plan/types.ts | 2 +- ...andbox-paths.windows-drive-resolve.test.ts | 2 +- src/agents/sandbox-tool-policy.test.ts | 2 +- ...ema-normalization-runtime-contract.test.ts | 4 +- src/agents/session-file-repair.ts | 2 +- src/agents/session-raw-append-message.ts | 2 +- src/agents/session-runtime-compat.ts | 56 + src/agents/session-suspension.ts | 2 +- .../session-tool-result-guard-wrapper.ts | 10 +- src/agents/session-tool-result-guard.test.ts | 6 +- ...ult-guard.tool-result-persist-hook.test.ts | 4 +- ...ool-result-guard.transcript-events.test.ts | 4 +- src/agents/session-tool-result-guard.ts | 8 +- ...sion-transcript-repair.attachments.test.ts | 2 +- src/agents/session-transcript-repair.test.ts | 2 +- src/agents/session-transcript-repair.ts | 2 +- src/agents/sessions/agent-session-runtime.ts | 441 +++ src/agents/sessions/agent-session-services.ts | 211 ++ src/agents/sessions/agent-session.ts | 3222 +++++++++++++++++ src/agents/sessions/auth-guidance.ts | 25 + src/agents/sessions/auth-storage.ts | 551 +++ src/agents/sessions/bash-executor.ts | 159 + .../compaction/branch-summarization.ts | 381 ++ src/agents/sessions/compaction/compaction.ts | 938 +++++ src/agents/sessions/compaction/index.ts | 7 + src/agents/sessions/compaction/utils.ts | 193 + src/agents/sessions/defaults.ts | 3 + src/agents/sessions/diagnostics.ts | 15 + src/agents/sessions/event-bus.ts | 33 + src/agents/sessions/exec.ts | 111 + src/agents/sessions/extension-sdk.ts | 56 + src/agents/sessions/extensions/index.ts | 172 + src/agents/sessions/extensions/loader.test.ts | 53 + src/agents/sessions/extensions/loader.ts | 654 ++++ src/agents/sessions/extensions/runner.ts | 1147 ++++++ src/agents/sessions/extensions/types.ts | 1660 +++++++++ src/agents/sessions/extensions/wrapper.ts | 36 + src/agents/sessions/footer-data-provider.ts | 388 ++ src/agents/sessions/http-dispatcher.ts | 55 + src/agents/sessions/index.ts | 43 + src/agents/sessions/keybindings.ts | 378 ++ src/agents/sessions/messages.ts | 210 ++ src/agents/sessions/model-registry.ts | 1036 ++++++ src/agents/sessions/model-resolver.ts | 676 ++++ src/agents/sessions/output-guard.ts | 84 + src/agents/sessions/package-manager.test.ts | 106 + src/agents/sessions/package-manager.ts | 1603 ++++++++ src/agents/sessions/prompt-templates.ts | 315 ++ src/agents/sessions/provider-display-names.ts | 32 + src/agents/sessions/resolve-config-value.ts | 153 + src/agents/sessions/resource-loader.ts | 1028 ++++++ src/agents/sessions/sdk.ts | 437 +++ src/agents/sessions/session-cwd.ts | 62 + src/agents/sessions/session-manager.ts | 1541 ++++++++ src/agents/sessions/settings-manager.ts | 1111 ++++++ src/agents/sessions/skills.ts | 526 +++ src/agents/sessions/slash-commands.ts | 40 + src/agents/sessions/source-info.ts | 40 + src/agents/sessions/system-prompt.ts | 179 + src/agents/sessions/telemetry.ts | 17 + src/agents/sessions/timings.ts | 37 + src/agents/sessions/tools/bash.ts | 504 +++ src/agents/sessions/tools/edit-diff.ts | 479 +++ src/agents/sessions/tools/edit.ts | 542 +++ .../sessions/tools/file-mutation-queue.ts | 39 + src/agents/sessions/tools/find.ts | 394 ++ src/agents/sessions/tools/grep.ts | 445 +++ src/agents/sessions/tools/index.ts | 211 ++ src/agents/sessions/tools/ls.ts | 250 ++ .../sessions/tools/output-accumulator.ts | 230 ++ src/agents/sessions/tools/path-utils.ts | 94 + src/agents/sessions/tools/read.ts | 423 +++ src/agents/sessions/tools/render-utils.ts | 79 + .../sessions/tools/tool-definition-wrapper.ts | 51 + src/agents/sessions/tools/truncate.ts | 275 ++ src/agents/sessions/tools/write.ts | 330 ++ src/agents/simple-completion-runtime.test.ts | 24 +- src/agents/simple-completion-runtime.ts | 23 +- .../simple-completion-transport.test.ts | 2 +- src/agents/simple-completion-transport.ts | 4 +- src/agents/skills/compact-format.test.ts | 2 +- src/agents/skills/skill-contract.ts | 4 +- src/agents/stream-message-shared.ts | 2 +- .../subagent-announce-delivery.runtime.ts | 8 +- src/agents/subagent-announce-delivery.test.ts | 140 +- src/agents/subagent-announce-delivery.ts | 52 +- .../subagent-announce.format.e2e.test.ts | 128 +- src/agents/subagent-announce.live.test.ts | 10 +- src/agents/subagent-announce.runtime.ts | 5 +- src/agents/subagent-announce.test-support.ts | 18 +- src/agents/subagent-announce.test.ts | 52 +- src/agents/subagent-announce.timeout.test.ts | 20 +- src/agents/subagent-announce.ts | 10 +- src/agents/subagent-control.runtime.ts | 2 +- src/agents/subagent-control.test.ts | 2 +- src/agents/subagent-control.ts | 19 +- .../subagent-registry-lifecycle.test.ts | 2 +- src/agents/subagent-registry-lifecycle.ts | 2 +- src/agents/subagent-spawn.workspace.test.ts | 6 +- src/agents/system-prompt-report.ts | 54 +- src/agents/system-prompt.test.ts | 8 +- src/agents/system-prompt.ts | 8 +- .../test-helpers/agent-message-fixtures.ts | 4 +- ...en-mock.ts => agent-session-token-mock.ts} | 10 +- .../{pi-tool-stubs.ts => agent-tool-stubs.ts} | 2 +- ...s-helpers.ts => agent-tools-fs-helpers.ts} | 0 ...ts => agent-tools-sandbox-context.test.ts} | 10 +- ...text.ts => agent-tools-sandbox-context.ts} | 6 +- .../assistant-message-fixtures.ts | 2 +- ... => embedded-agent-runner-e2e-fixtures.ts} | 18 +- ....ts => embedded-agent-runner-e2e-mocks.ts} | 6 +- .../test-helpers/unsafe-mounted-sandbox.ts | 4 +- src/agents/test-helpers/usage-fixtures.ts | 2 +- src/agents/tool-call-id.test.ts | 2 +- src/agents/tool-call-id.ts | 8 +- src/agents/tool-images.ts | 4 +- src/agents/tool-policy-pipeline.ts | 4 +- src/agents/tool-replay-repair.live.test.ts | 16 +- src/agents/tool-search.test.ts | 4 +- src/agents/tool-search.ts | 24 +- src/agents/tools-effective-inventory.test.ts | 6 +- src/agents/tools-effective-inventory.ts | 4 +- src/agents/tools/agent-step.test.ts | 2 +- src/agents/tools/agent-step.ts | 2 +- src/agents/tools/agents-list-tool.test.ts | 6 +- src/agents/tools/chat-history-text.ts | 2 +- src/agents/tools/common.ts | 10 +- .../tools/gateway-tool-guard-coverage.test.ts | 6 +- src/agents/tools/image-tool.helpers.ts | 4 +- src/agents/tools/image-tool.test.ts | 14 +- src/agents/tools/media-tool-shared.ts | 13 +- src/agents/tools/nodes-tool-media.ts | 2 +- src/agents/tools/pdf-native-providers.ts | 2 +- src/agents/tools/pdf-tool.helpers.ts | 4 +- src/agents/tools/pdf-tool.test.ts | 6 +- src/agents/tools/pdf-tool.ts | 2 +- src/agents/tools/sessions-send-helpers.ts | 4 +- src/agents/tools/tool-runtime.helpers.ts | 2 +- src/agents/transcript-policy.ts | 2 +- src/agents/transcript-redact.test.ts | 2 +- src/agents/transcript-redact.ts | 2 +- .../transport-message-transform.test.ts | 6 +- src/agents/transport-message-transform.ts | 8 +- .../transport-params-runtime-contract.test.ts | 10 +- src/agents/transport-stream-shared.ts | 2 +- src/agents/usage.test.ts | 2 +- src/agents/usage.ts | 2 +- src/agents/utils/ansi.ts | 60 + src/agents/utils/changelog.ts | 103 + src/agents/utils/child-process.ts | 127 + src/agents/utils/clipboard-image.ts | 303 ++ src/agents/utils/clipboard-native.ts | 23 + src/agents/utils/clipboard.ts | 131 + src/agents/utils/exif-orientation.ts | 220 ++ src/agents/utils/frontmatter.ts | 40 + src/agents/utils/fs-watch.ts | 30 + src/agents/utils/git.test.ts | 19 + src/agents/utils/git.ts | 233 ++ src/agents/utils/html.ts | 51 + src/agents/utils/image-convert.ts | 43 + src/agents/utils/image-resize.ts | 189 + src/agents/utils/mime.ts | 90 + src/agents/utils/paths.ts | 78 + src/agents/utils/photon.ts | 139 + src/agents/utils/shell.ts | 225 ++ src/agents/utils/sleep.ts | 18 + src/agents/utils/syntax-highlight.ts | 155 + src/agents/utils/tools-manager.ts | 415 +++ src/agents/xai.live.test.ts | 12 +- src/agents/zai.live.test.ts | 2 +- src/auto-reply/fallback-state.ts | 2 +- src/auto-reply/get-reply-options.types.ts | 2 +- src/auto-reply/handoff-summarizer.ts | 2 +- ...irective.directive-behavior.e2e-harness.ts | 10 +- ....directive.directive-behavior.e2e-mocks.ts | 36 +- ...rrent-verbose-level-verbose-has-no.test.ts | 16 +- src/auto-reply/reply.test-harness.ts | 36 +- ...ge-summary-current-model-provider.cases.ts | 10 +- ...ets-active-session-native-stop.e2e.test.ts | 120 +- src/auto-reply/reply/abort.test.ts | 18 +- src/auto-reply/reply/abort.ts | 13 +- src/auto-reply/reply/acp-projector.ts | 2 +- .../reply/agent-runner-cli-dispatch.ts | 6 +- .../reply/agent-runner-execution.test.ts | 226 +- .../reply/agent-runner-execution.ts | 53 +- .../reply/agent-runner-memory.dedup.test.ts | 2 +- .../reply/agent-runner-memory.test.ts | 361 +- src/auto-reply/reply/agent-runner-memory.ts | 51 +- src/auto-reply/reply/agent-runner-payloads.ts | 4 +- .../reply/agent-runner.media-paths.test.ts | 102 +- .../agent-runner.misc.runreplyagent.test.ts | 129 +- .../agent-runner.runreplyagent.e2e.test.ts | 148 +- src/auto-reply/reply/agent-runner.ts | 12 +- .../reply/commands-abort-trigger.test.ts | 8 +- .../reply/commands-compact.runtime.ts | 10 +- src/auto-reply/reply/commands-compact.test.ts | 42 +- src/auto-reply/reply/commands-compact.ts | 8 +- .../reply/commands-context-report.ts | 2 +- .../reply/commands-export-session.test.ts | 2 +- .../reply/commands-export-session.ts | 32 +- src/auto-reply/reply/commands-models.test.ts | 34 +- src/auto-reply/reply/commands-models.ts | 8 +- src/auto-reply/reply/commands-status.test.ts | 24 +- .../reply/commands-steer.runtime.ts | 10 +- src/auto-reply/reply/commands-steer.test.ts | 34 +- src/auto-reply/reply/commands-steer.ts | 12 +- .../reply/commands-stop-target.test.ts | 8 +- .../reply/commands-system-prompt.test.ts | 4 +- .../reply/commands-system-prompt.ts | 8 +- src/auto-reply/reply/commands-types.ts | 2 +- .../conversation-label-generator.test.ts | 6 +- .../reply/conversation-label-generator.ts | 4 +- src/auto-reply/reply/current-turn-images.ts | 4 +- .../reply/directive-handling.model.test.ts | 6 +- .../reply/directive-handling.persist.ts | 4 +- ...ispatch-from-config.shared.test-harness.ts | 2 +- .../reply/dispatch-from-config.test.ts | 2 +- src/auto-reply/reply/dispatch-from-config.ts | 18 +- src/auto-reply/reply/export-html/template.js | 4 +- src/auto-reply/reply/followup-delivery.ts | 2 +- src/auto-reply/reply/followup-runner.test.ts | 180 +- src/auto-reply/reply/followup-runner.ts | 32 +- .../reply/get-reply-inline-actions.ts | 2 +- .../reply/get-reply-run.media-only.test.ts | 104 +- src/auto-reply/reply/get-reply-run.ts | 40 +- .../reply/get-reply.fast-path.runtime.test.ts | 10 +- src/auto-reply/reply/model-selection.test.ts | 4 +- src/auto-reply/reply/normalize-reply.ts | 2 +- src/auto-reply/reply/prompt-prelude.ts | 2 +- src/auto-reply/reply/queue/cleanup.test.ts | 2 +- src/auto-reply/reply/queue/cleanup.ts | 2 +- src/auto-reply/reply/queue/types.ts | 2 +- src/auto-reply/reply/reply-payloads-dedupe.ts | 4 +- .../reply/runtime-plugins.runtime.ts | 1 - src/auto-reply/reply/session-fork.runtime.ts | 24 +- .../reply/session-hooks-context.test.ts | 2 +- .../reply/session-transcript-replay.ts | 2 +- src/auto-reply/reply/session.test.ts | 4 +- src/auto-reply/reply/session.ts | 2 +- .../reply/skill-tool-dispatch.runtime.ts | 6 +- src/auto-reply/status.test.ts | 34 +- .../plugins/message-action-dispatch.ts | 2 +- src/channels/plugins/types.core.ts | 4 +- src/cli/capability-cli.test.ts | 4 +- src/cli/capability-cli.ts | 4 +- src/cli/gateway-cli/lifecycle.runtime.ts | 4 +- src/cli/gateway-cli/run-loop.test.ts | 30 +- src/cli/gateway-cli/run-loop.ts | 10 +- src/cli/logs-cli.ts | 26 +- src/cli/models-cli.ts | 15 +- src/cli/plugins-cli.list.test.ts | 4 +- src/cli/plugins-cli.runtime.ts | 7 +- src/cli/skills-cli.test.ts | 2 +- src/commands/agent-command.test-mocks.ts | 6 +- src/commands/agent.acp.test.ts | 16 +- src/commands/agent.test.ts | 30 +- src/commands/auth-choice.test.ts | 2 - src/commands/chutes-oauth.ts | 2 +- src/commands/doctor-bootstrap-size.test.ts | 2 +- src/commands/doctor-bootstrap-size.ts | 2 +- src/commands/doctor-config-flow.test.ts | 4 +- src/commands/doctor-cron.test.ts | 4 +- .../doctor-legacy-config.migrations.test.ts | 4 +- .../doctor-session-state-providers.test.ts | 14 +- src/commands/doctor-state-integrity.test.ts | 4 - .../shared/codex-route-warnings.test.ts | 58 +- .../doctor/shared/codex-route-warnings.ts | 35 +- .../shared/context-engine-host-compat.test.ts | 6 +- .../shared/context-engine-host-compat.ts | 8 +- .../doctor/shared/deprecation-compat.test.ts | 1 + .../doctor/shared/deprecation-compat.ts | 16 +- .../shared/legacy-config-core-normalizers.ts | 2 +- .../shared/legacy-config-migrate.test.ts | 72 +- ...legacy-config-migrations.runtime.agents.ts | 71 +- .../missing-configured-plugin-install.ts | 1 - .../shared/plugin-tool-allowlist-warnings.ts | 2 +- .../doctor/shared/stale-plugin-config.test.ts | 2 +- src/commands/models.list.e2e.test.ts | 4 +- src/commands/onboard-auth.test.ts | 12 +- src/commands/onboard-custom-config.ts | 2 +- src/commands/sessions-cleanup.test.ts | 4 +- .../sessions.acp-runtime-metadata.test.ts | 22 +- .../sessions.default-agent-store.test.ts | 14 +- .../sessions.model-resolution.test.ts | 4 +- src/commands/sessions.test-helpers.ts | 4 +- src/commands/sessions.test.ts | 30 +- src/commands/sessions.ts | 2 +- src/commands/status.command-sections.test.ts | 6 +- src/commands/status.command.ts | 9 +- .../status.gateway-connection.runtime.ts | 1 - src/commands/status.summary.runtime.test.ts | 2 +- src/commands/status.summary.runtime.ts | 2 +- src/commands/status.summary.test.ts | 2 +- src/commands/status.test-support.ts | 2 +- src/commands/status.test.ts | 12 +- src/commitments/runtime.test.ts | 22 +- src/commitments/runtime.ts | 8 +- src/config/config-misc.test.ts | 4 +- src/config/config.compaction-settings.test.ts | 2 +- src/config/config.hooks-module-paths.test.ts | 12 +- src/config/config.plugin-validation.test.ts | 68 +- src/config/defaults.ts | 2 +- src/config/plugin-auto-enable.core.test.ts | 12 +- src/config/schema.help.ts | 62 +- src/config/schema.labels.ts | 17 +- src/config/sessions/store-load.ts | 2 +- .../sessions/store.skills-stripping.test.ts | 2 +- src/config/sessions/transcript-append.ts | 23 +- src/config/sessions/transcript-header.ts | 17 + src/config/sessions/transcript.ts | 24 +- src/config/sessions/types.ts | 4 +- src/config/types.agent-defaults.ts | 30 +- src/config/types.agents-shared.ts | 4 +- src/config/types.agents.ts | 24 +- src/config/types.models.ts | 2 +- src/config/zod-schema.agent-defaults.test.ts | 30 +- src/config/zod-schema.agent-defaults.ts | 40 +- src/config/zod-schema.agent-runtime.ts | 38 +- src/context-engine/context-engine.test.ts | 16 +- src/context-engine/delegate.ts | 14 +- src/context-engine/host-compat.test.ts | 8 +- src/context-engine/host-compat.ts | 6 +- src/context-engine/legacy.ts | 4 +- src/context-engine/types.ts | 4 +- src/crestodian/assistant.test.ts | 30 +- src/crestodian/assistant.ts | 12 +- ...ted-agent.auth-profile-propagation.test.ts | 20 +- .../isolated-agent.delivery.test-helpers.ts | 6 +- ...olated-agent.hook-content-wrapping.test.ts | 6 +- src/cron/isolated-agent.lane.test.ts | 8 +- src/cron/isolated-agent.mocks.ts | 8 +- .../isolated-agent.model-overrides.test.ts | 16 +- .../isolated-agent.model-preflight.test.ts | 4 +- ...solated-agent.run-timeout-override.test.ts | 10 +- .../isolated-agent.session-identity.test.ts | 14 +- src/cron/isolated-agent.test-setup.ts | 4 +- src/cron/isolated-agent.turn-test-helpers.ts | 8 +- .../delivery-dispatch.double-announce.test.ts | 4 +- src/cron/isolated-agent/delivery-dispatch.ts | 2 +- .../isolated-agent/run-embedded.runtime.ts | 2 +- src/cron/isolated-agent/run-executor.ts | 4 +- .../run-runtime-plugins.runtime.ts | 1 - ...run.cron-model-override-forwarding.test.ts | 12 +- src/cron/isolated-agent/run.fast-mode.test.ts | 6 +- .../isolated-agent/run.interim-retry.test.ts | 20 +- .../run.live-session-model-switch.test.ts | 10 +- .../run.message-tool-policy.test.ts | 62 +- .../run.payload-fallbacks.test.ts | 8 +- .../run.session-key-isolation.test.ts | 10 +- src/cron/isolated-agent/run.test-harness.ts | 14 +- .../isolated-agent/run.tools-allow.test.ts | 12 +- src/cron/isolated-agent/run.ts | 4 +- src/cron/service/timer.ts | 11 +- src/cron/types.ts | 4 +- src/extensionAPI.ts | 6 +- src/flows/doctor-core-checks.ts | 2 +- .../gateway-cli-backend.live-helpers.ts | 2 +- .../gateway-models.profiles.live.test.ts | 47 +- src/gateway/mcp-http.handlers.ts | 2 +- src/gateway/mcp-http.test.ts | 2 +- src/gateway/openai-compat-errors.ts | 2 +- src/gateway/openai-http.test.ts | 10 +- src/gateway/openai-http.ts | 2 +- src/gateway/openresponses-http.test.ts | 2 +- src/gateway/openresponses-http.ts | 4 +- .../protocol/schema/agents-models-skills.ts | 2 +- src/gateway/server-close.test.ts | 12 +- src/gateway/server-close.ts | 4 +- src/gateway/server-cron.ts | 4 +- src/gateway/server-methods/AGENTS.md | 2 +- src/gateway/server-methods/agent.ts | 2 +- .../server-methods/chat-transcript-inject.ts | 4 +- .../chat.abort-persistence.test.ts | 2 +- .../chat.directive-tags.test.ts | 6 +- .../chat.inject.parentid.test.ts | 2 +- .../server-methods/chat.test-helpers.ts | 2 +- src/gateway/server-methods/chat.ts | 14 +- src/gateway/server-methods/sessions.ts | 22 +- src/gateway/server-reload-handlers.test.ts | 2 +- src/gateway/server-reload-handlers.ts | 12 +- .../server-startup-post-attach.test.ts | 16 + src/gateway/server-startup-post-attach.ts | 152 + src/gateway/server-startup.test.ts | 195 + ...erver.agent.gateway-server-agent-a.test.ts | 6 +- src/gateway/server.impl.ts | 2 +- .../server.models-voicewake-misc.test.ts | 44 +- .../server.sessions.compaction.test.ts | 10 +- src/gateway/server.sessions.create.test.ts | 6 +- src/gateway/server.sessions.store-rpc.test.ts | 8 +- .../session-compaction-checkpoints.test.ts | 4 +- src/gateway/session-compaction-checkpoints.ts | 16 +- src/gateway/session-reset-service.ts | 10 +- src/gateway/session-utils.fs.test.ts | 2 +- src/gateway/sessions-history-http.test.ts | 2 +- src/gateway/test-helpers.mocks.ts | 68 +- src/gateway/test-helpers.runtime-state.ts | 12 +- src/gateway/test-helpers.server.ts | 18 +- src/gateway/test-helpers.ts | 2 +- .../test/server-sessions.test-helpers.ts | 12 +- src/gateway/tool-resolution.ts | 4 +- .../tools-invoke-http.cron-regression.test.ts | 4 +- src/gateway/tools-invoke-http.test.ts | 6 +- src/gateway/tools-invoke-shared.ts | 4 +- .../bundled/session-memory/handler.test.ts | 2 +- src/hooks/llm-slug-generator.test.ts | 22 +- src/hooks/llm-slug-generator.ts | 4 +- src/infra/abort-pattern.test.ts | 2 +- src/infra/dotenv.test.ts | 5 +- src/infra/dotenv.ts | 1 - .../heartbeat-runner.tool-response.test.ts | 17 +- src/infra/heartbeat-runner.ts | 56 +- src/infra/outbound/message-action-runner.ts | 2 +- src/infra/outbound/outbound-send-service.ts | 2 +- ...rovider-usage.auth.normalizes-keys.test.ts | 55 +- src/infra/provider-usage.auth.plugin.test.ts | 57 - src/infra/provider-usage.shared.test.ts | 38 +- src/infra/restart-coordinator.ts | 2 +- src/infra/state-migrations.ts | 2 +- src/infra/tsdown-config.test.ts | 1 - ...stuck-session-recovery.integration.test.ts | 2 +- ...tic-stuck-session-recovery.runtime.test.ts | 262 +- ...agnostic-stuck-session-recovery.runtime.ts | 35 +- src/mcp/plugin-tools-handlers.ts | 2 +- src/media-understanding/apply.test.ts | 3 - src/media-understanding/image.test.ts | 14 +- src/media-understanding/image.ts | 34 +- .../runner.auto-audio.test.ts | 1 - src/media-understanding/runner.video.test.ts | 1 - src/media/read-capability.ts | 2 +- src/plugin-sdk/agent-core.ts | 1 + src/plugin-sdk/agent-dir-compat.ts | 2 +- src/plugin-sdk/agent-harness-runtime.ts | 68 +- src/plugin-sdk/agent-harness.ts | 2 +- src/plugin-sdk/agent-runtime.ts | 4 +- src/plugin-sdk/agent-sessions.ts | 1 + src/plugin-sdk/api-baseline.test.ts | 4 +- .../memory-core-host-runtime-core.ts | 6 +- src/plugin-sdk/memory-core-host-secret.ts | 2 +- src/plugin-sdk/provider-stream-shared.test.ts | 215 +- src/plugin-sdk/provider-stream.test.ts | 2 +- src/plugin-sdk/provider-usage.test.ts | 39 + src/plugin-sdk/provider-usage.ts | 6 + src/plugin-sdk/simple-completion-runtime.ts | 2 +- src/plugin-sdk/test-env.ts | 2 +- .../openclaw-owned-tool-runtime-contract.ts | 4 +- .../outcome-fallback-runtime-contract.ts | 6 +- .../transcript-repair-runtime-contract.ts | 2 +- .../test-helpers/plugin-runtime-mock.ts | 14 +- .../test-helpers/provider-runtime-contract.ts | 38 +- src/plugin-sdk/test-helpers/stream-hooks.ts | 2 +- src/plugin-sdk/testing.ts | 2 +- src/plugin-sdk/tool-plugin.ts | 4 +- .../agent-tool-result-middleware-types.ts | 11 +- .../agent-tool-result-middleware.test.ts | 13 +- src/plugins/agent-tool-result-middleware.ts | 15 +- src/plugins/channel-plugin-ids.test.ts | 31 +- .../codex-app-server-extension-types.ts | 2 +- src/plugins/command-registration.ts | 9 +- src/plugins/command-registry-state.ts | 8 +- src/plugins/commands.test.ts | 8 +- src/plugins/compat/registry.test.ts | 22 + src/plugins/compat/registry.ts | 25 +- src/plugins/contracts/tts-contract-suites.ts | 13 +- src/plugins/gateway-startup-plugin-ids.ts | 165 +- src/plugins/hook-types.ts | 2 +- src/plugins/hooks.sync-only.test.ts | 2 +- src/plugins/host-hook-turn-types.ts | 2 +- src/plugins/pi-package-graph.test.ts | 95 - src/plugins/provider-auth-helpers.ts | 2 +- src/plugins/provider-model-compat.ts | 8 +- src/plugins/provider-model-helpers.test.ts | 2 +- .../provider-openai-codex-oauth.test.ts | 8 +- src/plugins/provider-openai-codex-oauth.ts | 2 +- src/plugins/provider-replay-helpers.ts | 2 +- src/plugins/provider-runtime-model.types.ts | 4 +- src/plugins/provider-runtime.test.ts | 2 +- src/plugins/runtime-plugins.runtime.ts | 1 + src/plugins/runtime/index.test.ts | 1 + src/plugins/runtime/runtime-agent.ts | 12 +- .../runtime/runtime-embedded-agent.runtime.ts | 1 + .../runtime/runtime-embedded-pi.runtime.ts | 1 - .../runtime/runtime-llm.runtime.test.ts | 2 +- src/plugins/runtime/runtime-llm.runtime.ts | 2 +- .../runtime/runtime-model-auth.runtime.ts | 4 +- .../runtime/runtime-web-channel-plugin.ts | 2 +- src/plugins/runtime/types-core.ts | 15 +- src/plugins/types.ts | 16 +- .../wired-hooks-after-tool-call.e2e.test.ts | 10 +- src/plugins/wired-hooks-compaction.test.ts | 2 +- src/process/exec.ts | 2 +- src/scripts/control-ui-i18n.test.ts | 44 +- src/secrets/runtime-fast-path.ts | 1 - src/secrets/storage-scan.ts | 4 +- src/security/audit-extra.summary.ts | 2 +- src/sessions/input-provenance.ts | 2 +- src/shared/google-turn-ordering.ts | 2 +- src/shared/node-match.test.ts | 2 +- src/shared/session-types.ts | 2 +- src/status/agent-runtime-label.ts | 4 +- src/status/status-message.ts | 2 +- src/talk/agent-consult-runtime.test.ts | 40 +- src/talk/agent-consult-runtime.ts | 12 +- src/tasks/task-status.ts | 2 +- src/test-utils/openclaw-test-state.test.ts | 15 - src/test-utils/openclaw-test-state.ts | 3 - src/trajectory/export.test.ts | 2 +- src/trajectory/export.ts | 4 +- src/trajectory/metadata.test.ts | 2 +- src/tts/tts-core.ts | 4 +- src/tui/tui-event-handlers.ts | 2 +- src/tui/tui-pty-harness.e2e.test.ts | 2 +- .../{pi-agent-core.d.ts => agent-core.d.ts} | 4 +- src/types/agent-sessions.d.ts | 8 + src/types/highlight-js-lib-index.d.ts | 19 + src/types/pi-coding-agent.d.ts | 8 - .../agents/happy-path-prompt-snapshots.ts | 2 +- ...mple-mock.ts => llm-stream-simple-mock.ts} | 4 +- .../agents/prompt-composition-scenarios.ts | 6 +- test/helpers/auth-wizard.ts | 1 - .../trigger-handling-test-harness.ts | 106 +- .../heartbeat-config-honor.inventory.ts | 2 +- test/image-generation.runtime.live.test.ts | 2 +- test/non-isolated-runner.ts | 1 - test/scripts/ci-node-test-plan.test.ts | 2 +- test/scripts/docker-e2e-plan.test.ts | 4 +- test/scripts/generate-npm-shrinkwrap.test.ts | 31 +- test/scripts/lint-suppressions.test.ts | 2 +- .../openclaw-cross-os-release-checks.test.ts | 2 +- test/scripts/parallels-smoke-model.test.ts | 2 +- test/scripts/root-package-overrides.test.ts | 3 +- test/scripts/test-projects.test.ts | 2 +- .../transitive-manifest-risk-report.test.ts | 6 +- test/setup.shared.ts | 2 +- test/test-env.ts | 2 - test/vitest-projects-config.test.ts | 4 +- test/vitest-unit-fast-config.test.ts | 4 +- test/vitest-unit-paths.test.ts | 2 +- .../vitest.agents-embedded-agent.config.ts | 12 + test/vitest/vitest.agents-paths.mjs | 4 +- .../vitest.agents-pi-embedded.config.ts | 12 - test/vitest/vitest.config.ts | 2 +- test/vitest/vitest.scoped-config.ts | 2 +- test/vitest/vitest.shared.config.ts | 6 +- test/vitest/vitest.test-shards.mjs | 2 +- 1339 files changed, 38617 insertions(+), 12744 deletions(-) create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 docs/agent-runtime-architecture.md rename docs/{pi-dev.md => openclaw-agent-runtime.md} (57%) rename qa/scenarios/runtime/{codex-pi-shaped-read-vocabulary.md => codex-legacy-read-tool-vocabulary.md} (75%) rename qa/scenarios/workspace/{medium-game-plan-pi-harness.md => medium-game-plan-openclaw-harness.md} (82%) rename scripts/e2e/{pi-bundle-mcp-tools-docker-client.ts => agent-bundle-mcp-tools-docker-client.ts} (86%) rename scripts/e2e/{pi-bundle-mcp-tools-docker.sh => agent-bundle-mcp-tools-docker.sh} (61%) rename src/agents/{pi-auth-credentials.ts => agent-auth-credentials.ts} (70%) rename src/agents/{pi-auth-discovery-core.ts => agent-auth-discovery-core.ts} (86%) rename src/agents/{pi-auth-discovery.external-cli.test.ts => agent-auth-discovery.external-cli.test.ts} (84%) rename src/agents/{pi-auth-discovery.ts => agent-auth-discovery.ts} (82%) rename src/agents/{pi-auth-json.test.ts => agent-auth-json.test.ts} (87%) rename src/agents/{pi-auth-json.ts => agent-auth-json.ts} (74%) rename src/agents/{pi-bundle-lsp-runtime.test.ts => agent-bundle-lsp-runtime.test.ts} (88%) rename src/agents/{pi-bundle-lsp-runtime.ts => agent-bundle-lsp-runtime.ts} (98%) rename src/agents/{pi-bundle-lsp-runtime.windows-spawn.test.ts => agent-bundle-lsp-runtime.windows-spawn.test.ts} (95%) rename src/agents/{pi-bundle-mcp-materialize.ts => agent-bundle-mcp-materialize.ts} (94%) rename src/agents/{pi-bundle-mcp-names.test.ts => agent-bundle-mcp-names.test.ts} (95%) rename src/agents/{pi-bundle-mcp-names.ts => agent-bundle-mcp-names.ts} (100%) rename src/agents/{pi-bundle-mcp-runtime.test.ts => agent-bundle-mcp-runtime.test.ts} (98%) rename src/agents/{pi-bundle-mcp-runtime.ts => agent-bundle-mcp-runtime.ts} (98%) rename src/agents/{pi-bundle-mcp-test-harness.ts => agent-bundle-mcp-test-harness.ts} (63%) rename src/agents/{pi-bundle-mcp-tools.materialize.test.ts => agent-bundle-mcp-tools.materialize.test.ts} (96%) rename src/agents/{pi-bundle-mcp-tools.request-boundary.test.ts => agent-bundle-mcp-tools.request-boundary.test.ts} (94%) rename src/agents/{pi-bundle-mcp-tools.ts => agent-bundle-mcp-tools.ts} (79%) rename src/agents/{pi-bundle-mcp-types.ts => agent-bundle-mcp-types.ts} (100%) rename src/agents/{pi-compaction-constants.ts => agent-compaction-constants.ts} (100%) rename src/agents/{pi-hooks => agent-hooks}/compaction-instructions.test.ts (100%) rename src/agents/{pi-hooks => agent-hooks}/compaction-instructions.ts (100%) rename src/agents/{pi-hooks => agent-hooks}/compaction-safeguard-quality.ts (100%) rename src/agents/{pi-hooks => agent-hooks}/compaction-safeguard-runtime.ts (96%) rename src/agents/{pi-hooks => agent-hooks}/compaction-safeguard.test.ts (99%) rename src/agents/{pi-hooks => agent-hooks}/compaction-safeguard.ts (99%) rename src/agents/{pi-hooks => agent-hooks}/context-pruning.test.ts (98%) rename src/agents/{pi-hooks => agent-hooks}/context-pruning.ts (83%) rename src/agents/{pi-hooks => agent-hooks}/context-pruning/extension.ts (97%) rename src/agents/{pi-hooks => agent-hooks}/context-pruning/pruner.test.ts (98%) rename src/agents/{pi-hooks => agent-hooks}/context-pruning/pruner.ts (97%) rename src/agents/{pi-hooks => agent-hooks}/context-pruning/runtime.ts (87%) rename src/agents/{pi-hooks => agent-hooks}/context-pruning/settings.ts (100%) rename src/agents/{pi-hooks => agent-hooks}/context-pruning/tools.ts (100%) rename src/agents/{pi-hooks => agent-hooks}/session-manager-runtime-registry.ts (100%) rename src/agents/{pi-mcp-style.cache.live.test.ts => agent-mcp-style.cache.live.test.ts} (98%) rename src/agents/{pi-model-discovery.auth.test.ts => agent-model-discovery.auth.test.ts} (92%) create mode 100644 src/agents/agent-model-discovery.internal.test.ts rename src/agents/{pi-model-discovery.synthetic-auth.test.ts => agent-model-discovery.synthetic-auth.test.ts} (80%) rename src/agents/{pi-model-discovery.test.ts => agent-model-discovery.test.ts} (85%) create mode 100644 src/agents/agent-model-discovery.ts rename src/agents/{pi-project-settings-snapshot.ts => agent-project-settings-snapshot.ts} (69%) rename src/agents/{pi-project-settings.bundle.test.ts => agent-project-settings.bundle.test.ts} (93%) rename src/agents/{pi-project-settings.test.ts => agent-project-settings.test.ts} (68%) rename src/agents/{pi-project-settings.ts => agent-project-settings.ts} (63%) create mode 100644 src/agents/agent-runtime-id.ts rename src/agents/{pi-settings.test.ts => agent-settings.test.ts} (81%) rename src/agents/{pi-settings.ts => agent-settings.ts} (86%) rename src/agents/{pi-tool-definition-adapter.after-tool-call.fires-once.test.ts => agent-tool-definition-adapter.after-tool-call.fires-once.test.ts} (93%) rename src/agents/{pi-tool-definition-adapter.after-tool-call.test.ts => agent-tool-definition-adapter.after-tool-call.test.ts} (94%) rename src/agents/{pi-tool-definition-adapter.logging.test.ts => agent-tool-definition-adapter.logging.test.ts} (93%) rename src/agents/{pi-tool-definition-adapter.test.ts => agent-tool-definition-adapter.test.ts} (96%) rename src/agents/{pi-tool-definition-adapter.ts => agent-tool-definition-adapter.ts} (96%) rename src/agents/{pi-tool-handler-state.test-helpers.ts => agent-tool-handler-state.test-helpers.ts} (92%) rename src/agents/{pi-tools-agent-config.exec.test.ts => agent-tools-agent-config.exec.test.ts} (98%) rename src/agents/{pi-tools-agent-config.test.ts => agent-tools-agent-config.test.ts} (99%) rename src/agents/{pi-tools-parameter-schema.ts => agent-tools-parameter-schema.ts} (100%) rename src/agents/{pi-tools.abort.ts => agent-tools.abort.ts} (93%) rename src/agents/{pi-tools.availability.test.ts => agent-tools.availability.test.ts} (96%) rename src/agents/{pi-tools.before-tool-call.e2e.test.ts => agent-tools.before-tool-call.e2e.test.ts} (99%) rename src/agents/{pi-tools.before-tool-call.embedded-mode.test.ts => agent-tools.before-tool-call.embedded-mode.test.ts} (99%) rename src/agents/{pi-tools.before-tool-call.integration.e2e.test.ts => agent-tools.before-tool-call.integration.e2e.test.ts} (99%) rename src/agents/{pi-tools.before-tool-call.runtime.ts => agent-tools.before-tool-call.runtime.ts} (100%) rename src/agents/{pi-tools.before-tool-call.state.ts => agent-tools.before-tool-call.state.ts} (100%) rename src/agents/{pi-tools.before-tool-call.ts => agent-tools.before-tool-call.ts} (99%) rename src/agents/{pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts => agent-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts} (98%) rename src/agents/{pi-tools.create-openclaw-coding-tools.test.ts => agent-tools.create-openclaw-coding-tools.test.ts} (98%) rename src/agents/{pi-tools.cron-scope.test.ts => agent-tools.cron-scope.test.ts} (96%) rename src/agents/{pi-tools.deferred-followup-guidance.test.ts => agent-tools.deferred-followup-guidance.test.ts} (95%) rename src/agents/{pi-tools.deferred-followup.ts => agent-tools.deferred-followup.ts} (91%) rename src/agents/{pi-tools.host-edit.ts => agent-tools.host-edit.ts} (97%) rename src/agents/{pi-tools.message-provider-policy.test.ts => agent-tools.message-provider-policy.test.ts} (88%) rename src/agents/{pi-tools.message-provider-policy.ts => agent-tools.message-provider-policy.ts} (100%) rename src/agents/{pi-tools.model-provider-collision.test.ts => agent-tools.model-provider-collision.test.ts} (98%) rename src/agents/{pi-tools.params.test.ts => agent-tools.params.test.ts} (99%) rename src/agents/{pi-tools.params.ts => agent-tools.params.ts} (98%) rename src/agents/{pi-tools.policy.test.ts => agent-tools.policy.test.ts} (98%) rename src/agents/{pi-tools.policy.ts => agent-tools.policy.ts} (99%) rename src/agents/{pi-tools.read.host-edit-access.test.ts => agent-tools.read.host-edit-access.test.ts} (90%) rename src/agents/{pi-tools.read.host-edit-recovery.test.ts => agent-tools.read.host-edit-recovery.test.ts} (99%) rename src/agents/{pi-tools.read.host-tilde-expansion.test.ts => agent-tools.read.host-tilde-expansion.test.ts} (96%) rename src/agents/{pi-tools.read.ts => agent-tools.read.ts} (99%) rename src/agents/{pi-tools.read.workspace-root-guard.test.ts => agent-tools.read.workspace-root-guard.test.ts} (96%) rename src/agents/{pi-tools.safe-bins.test.ts => agent-tools.safe-bins.test.ts} (98%) rename src/agents/{pi-tools.sandbox-mounted-paths.workspace-only.test.ts => agent-tools.sandbox-mounted-paths.workspace-only.test.ts} (98%) rename src/agents/{pi-tools.schema.test.ts => agent-tools.schema.test.ts} (99%) rename src/agents/{pi-tools.schema.ts => agent-tools.schema.ts} (96%) rename src/agents/{pi-tools.ts => agent-tools.ts} (98%) rename src/agents/{pi-tools.types.ts => agent-tools.types.ts} (100%) rename src/agents/{pi-tools.workspace-only-false.test.ts => agent-tools.workspace-only-false.test.ts} (95%) rename src/agents/{pi-tools.workspace-paths.test.ts => agent-tools.workspace-paths.test.ts} (98%) create mode 100644 src/agents/config.ts rename src/agents/{pi-embedded-block-chunker.test.ts => embedded-agent-block-chunker.test.ts} (98%) rename src/agents/{pi-embedded-block-chunker.ts => embedded-agent-block-chunker.ts} (100%) rename src/agents/{pi-embedded-error-observation.test.ts => embedded-agent-error-observation.test.ts} (99%) rename src/agents/{pi-embedded-error-observation.ts => embedded-agent-error-observation.ts} (99%) rename src/agents/{pi-embedded-helpers.buildbootstrapcontextfiles.test.ts => embedded-agent-helpers.buildbootstrapcontextfiles.test.ts} (99%) rename src/agents/{pi-embedded-helpers.formatassistanterrortext.test.ts => embedded-agent-helpers.formatassistanterrortext.test.ts} (99%) rename src/agents/{pi-embedded-helpers.isbillingerrormessage.test.ts => embedded-agent-helpers.isbillingerrormessage.test.ts} (99%) rename src/agents/{pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts => embedded-agent-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts} (99%) rename src/agents/{pi-embedded-helpers.sanitizeuserfacingtext.test.ts => embedded-agent-helpers.sanitizeuserfacingtext.test.ts} (99%) rename src/agents/{pi-embedded-helpers.ts => embedded-agent-helpers.ts} (73%) rename src/agents/{pi-embedded-helpers.validate-turns.test.ts => embedded-agent-helpers.validate-turns.test.ts} (99%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/bootstrap.test.ts (100%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/bootstrap.ts (99%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/errors.test.ts (98%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/errors.ts (99%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/failover-matches.test.ts (100%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/failover-matches.ts (99%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/google.ts (100%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/images.ts (98%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/messaging-dedupe.ts (100%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/openai.ts (98%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/provider-error-patterns.test.ts (100%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/provider-error-patterns.ts (100%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/sanitize-user-facing-text.ts (100%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/thinking.test.ts (100%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/thinking.ts (100%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/turns.ts (99%) rename src/agents/{pi-embedded-helpers => embedded-agent-helpers}/types.ts (100%) rename src/agents/{embedded-pi-lsp.ts => embedded-agent-lsp.ts} (85%) rename src/agents/{embedded-pi-mcp.ts => embedded-agent-mcp.ts} (83%) rename src/agents/{pi-embedded-messaging.ts => embedded-agent-messaging.ts} (100%) rename src/agents/{pi-embedded-messaging.types.ts => embedded-agent-messaging.types.ts} (100%) rename src/agents/{pi-embedded-payloads.ts => embedded-agent-payloads.ts} (100%) rename src/agents/{pi-embedded-runner-extraparams-moonshot.test.ts => embedded-agent-runner-extraparams-moonshot.test.ts} (95%) rename src/agents/{pi-embedded-runner-extraparams-openrouter.test.ts => embedded-agent-runner-extraparams-openrouter.test.ts} (95%) rename src/agents/{pi-embedded-runner-extraparams-resolve.test.ts => embedded-agent-runner-extraparams-resolve.test.ts} (98%) rename src/agents/{pi-embedded-runner-extraparams.live.test.ts => embedded-agent-runner-extraparams.live.test.ts} (93%) rename src/agents/{pi-embedded-runner-extraparams.test-support.ts => embedded-agent-runner-extraparams.test-support.ts} (82%) rename src/agents/{pi-embedded-runner-extraparams.test.ts => embedded-agent-runner-extraparams.test.ts} (98%) rename src/agents/{pi-embedded-runner.anthropic-tool-replay.live.test.ts => embedded-agent-runner.anthropic-tool-replay.live.test.ts} (95%) rename src/agents/{pi-embedded-runner.buildembeddedsandboxinfo.test.ts => embedded-agent-runner.buildembeddedsandboxinfo.test.ts} (96%) rename src/agents/{pi-embedded-runner.cache.live.test.ts => embedded-agent-runner.cache.live.test.ts} (99%) rename src/agents/{pi-embedded-runner.compaction-safety-timeout.test.ts => embedded-agent-runner.compaction-safety-timeout.test.ts} (99%) rename src/agents/{pi-embedded-runner.createsystempromptoverride.test.ts => embedded-agent-runner.createsystempromptoverride.test.ts} (85%) rename src/agents/{pi-embedded-runner.e2e.test.ts => embedded-agent-runner.e2e.test.ts} (87%) rename src/agents/{pi-embedded-runner.extensions.test.ts => embedded-agent-runner.extensions.test.ts} (95%) rename src/agents/{pi-embedded-runner.guard.test.ts => embedded-agent-runner.guard.test.ts} (98%) rename src/agents/{pi-embedded-runner.guard.waitforidle-before-flush.test.ts => embedded-agent-runner.guard.waitforidle-before-flush.test.ts} (96%) rename src/agents/{pi-embedded-runner.limithistoryturns.test.ts => embedded-agent-runner.limithistoryturns.test.ts} (96%) rename src/agents/{pi-embedded-runner.openai-tool-id-preservation.test.ts => embedded-agent-runner.openai-tool-id-preservation.test.ts} (95%) rename src/agents/{pi-embedded-runner.resolvesessionagentids.test.ts => embedded-agent-runner.resolvesessionagentids.test.ts} (100%) rename src/agents/{pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts => embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts} (96%) rename src/agents/{pi-embedded-runner.sanitize-session-history.policy.test.ts => embedded-agent-runner.sanitize-session-history.policy.test.ts} (96%) rename src/agents/{pi-embedded-runner.sanitize-session-history.test-harness.ts => embedded-agent-runner.sanitize-session-history.test-harness.ts} (93%) rename src/agents/{pi-embedded-runner.sanitize-session-history.test.ts => embedded-agent-runner.sanitize-session-history.test.ts} (99%) rename src/agents/{pi-embedded-runner.splitsdktools.test.ts => embedded-agent-runner.splitsdktools.test.ts} (82%) create mode 100644 src/agents/embedded-agent-runner.ts rename src/agents/{pi-embedded-runner => embedded-agent-runner}/abort.ts (100%) create mode 100644 src/agents/embedded-agent-runner/aliases.test.ts rename src/agents/{pi-embedded-runner => embedded-agent-runner}/cache-ttl.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/cache-ttl.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compact-reasons.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compact-reasons.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compact.hooks.harness.ts (95%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compact.hooks.test.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compact.queued.ts (93%) create mode 100644 src/agents/embedded-agent-runner/compact.runtime.ts create mode 100644 src/agents/embedded-agent-runner/compact.runtime.types.ts rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compact.ts (94%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compact.types.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compaction-duplicate-user-messages.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compaction-duplicate-user-messages.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compaction-hooks.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compaction-runtime-context.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compaction-runtime-context.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compaction-safety-timeout.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compaction-successor-transcript.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/compaction-successor-transcript.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/context-engine-capabilities.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/context-engine-maintenance.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/context-engine-maintenance.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/context-truncation-notice.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/delivery-evidence.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/effective-tool-policy.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/effective-tool-policy.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/empty-assistant-turn.ts (95%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/execution-phase.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/extensions.test.ts (90%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/extensions.ts (85%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/extra-params.cache-retention-default.test.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/extra-params.google.test.ts (93%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/extra-params.kilocode.test.ts (95%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/extra-params.openrouter-cache-control.test.ts (95%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/extra-params.provider-runtime.test.ts (94%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/extra-params.sampling.test.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/extra-params.test-support.ts (94%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/extra-params.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/extra-params.zai-tool-stream.test.ts (95%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/failure-signal.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/failure-signal.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/google-prompt-cache.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/google-prompt-cache.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/history.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/history.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/kilocode.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/lanes.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/lanes.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/logger.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/manual-compaction-boundary.test.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/manual-compaction-boundary.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/message-action-discovery-input.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/message-action-discovery-input.ts (100%) create mode 100644 src/agents/embedded-agent-runner/model-context-tokens.ts rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model-discovery-cache.ts (86%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.forward-compat.errors-and-overrides.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.forward-compat.test-support.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.forward-compat.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.inline-provider.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.inline-provider.ts (99%) create mode 100644 src/agents/embedded-agent-runner/model.provider-normalization.ts rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.provider-runtime.test-support.ts (100%) rename src/agents/{pi-embedded-runner/model.skip-pi-discovery-hooks.test.ts => embedded-agent-runner/model.skip-agent-discovery-hooks.test.ts} (92%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.startup-retry.test.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.static-catalog.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.static-catalog.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.test-harness.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.test.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/model.ts (94%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/openrouter-model-capabilities.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/openrouter-model-capabilities.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/post-compaction-loop-guard.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/post-compaction-loop-guard.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/prompt-cache-observability.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/prompt-cache-observability.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/prompt-cache-retention.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/prompt-cache-retention.ts (91%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/replay-history.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/replay-history.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/replay-state.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/resource-loader.test.ts (63%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/resource-loader.ts (65%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/result-fallback-classifier.test.ts (68%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/result-fallback-classifier.ts (91%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run-state.ts (91%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.before-agent-reply-cron.test.ts (92%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.codex-app-server-recovery.test.ts (94%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.codex-server-error-fallback.test.ts (90%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.compaction-loop-guard.test.ts (95%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.cross-provider-fallback-error-context.test.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.empty-error-retry.test.ts (93%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.incomplete-turn.test.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.overflow-compaction.fixture.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.overflow-compaction.harness.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.overflow-compaction.loop.test.ts (94%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.overflow-compaction.test.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.timeout-triggered-compaction.test.ts (94%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/AGENTS.md (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/CLAUDE.md (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/abortable.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/abortable.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/assistant-failover.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/assistant-failover.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt-bootstrap-routing.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt-http-runtime.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt-session.ts (89%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt-stage-timing.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt-stage-timing.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt-system-prompt.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt-system-prompt.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt-tool-construction-plan.test.ts (92%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt-tool-construction-plan.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt-trajectory-status.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt-trajectory-status.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.context-engine-helpers.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.memory-flush-forwarding.test.ts (93%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.model-diagnostic-events.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.model-diagnostic-events.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.prompt-helpers.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.prompt-helpers.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.queue-message.test.ts (91%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.session-lock.test.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.session-lock.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.sessions-yield.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.spawn-workspace.bootstrap-marker.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.spawn-workspace.bootstrap-routing.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.spawn-workspace.bootstrap-warning.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.spawn-workspace.cache-ttl.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.spawn-workspace.context-engine.test.ts (93%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.spawn-workspace.context-injection.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.spawn-workspace.resource-loader.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.spawn-workspace.sessions-spawn.test.ts (86%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.spawn-workspace.test-support.ts (92%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.spawn-workspace.timeout.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.stop-reason-recovery.test.ts (93%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.stop-reason-recovery.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.subscription-cleanup.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.subscription-cleanup.ts (94%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.thread-helpers.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.tool-call-argument-repair.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.tool-call-argument-repair.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.tool-call-normalization.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.tool-call-normalization.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.tool-run-context.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.transcript-policy.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.transcript-policy.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/attempt.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/auth-controller.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/auth-controller.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/auth-profile-failure-policy.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/auth-profile-failure-policy.ts (94%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/auth-profile-failure-policy.types.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/backend.test.ts (74%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/backend.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/codex-app-server-recovery.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/compaction-retry-aggregate-timeout.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/compaction-retry-aggregate-timeout.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/compaction-timeout.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/compaction-timeout.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/failover-observation.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/failover-observation.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/failover-policy.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/failover-policy.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/fallbacks.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/fallbacks.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/helpers.resolve-error-context.test.ts (86%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/helpers.test.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/helpers.ts (95%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/history-image-prune.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/history-image-prune.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/idle-timeout-breaker.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/idle-timeout-breaker.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/images.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/images.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/incomplete-turn.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/llm-idle-timeout.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/llm-idle-timeout.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/message-merge-strategy.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/message-merge-strategy.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/message-tool-terminal.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/message-tool-terminal.ts (93%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/midturn-precheck.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/params.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/payloads.errors.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/payloads.test-helpers.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/payloads.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/payloads.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/preemptive-compaction.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/preemptive-compaction.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/preemptive-compaction.types.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/retry-limit.ts (90%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/runtime-context-prompt.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/runtime-context-prompt.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/setup.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/setup.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/stream-wrapper.ts (94%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/tool-media-payloads.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/tool-media-payloads.ts (80%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/transcript-repair-runtime-contract.test.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/trigger-policy.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/trigger-policy.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/run/types.ts (94%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/runs.test.ts (88%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/runs.ts (87%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/sandbox-info.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/sanitize-session-history.tool-result-details.test.ts (93%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/session-file-key.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/session-manager-cache.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/session-manager-cache.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/session-manager-init.ts (97%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/sessions-yield.orchestration.test.ts (87%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/skills-runtime.integration.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/skills-runtime.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/skills-runtime.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/stream-resolution.test.ts (89%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/stream-resolution.ts (85%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/system-prompt.test.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/system-prompt.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/thinking.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/thinking.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-call-argument-decoding.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-name-allowlist.test.ts (87%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-name-allowlist.ts (75%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-result-char-estimator.test.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-result-char-estimator.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-result-context-guard.test.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-result-context-guard.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-result-truncation.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-result-truncation.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-schema-runtime.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-schema-runtime.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/tool-split.ts (70%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/transcript-file-state.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/transcript-file-state.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/transcript-rewrite.test.ts (99%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/transcript-rewrite.ts (98%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/types.ts (96%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/usage-accumulator.test.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/usage-accumulator.ts (100%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/usage-reporting.test.ts (91%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/utils.ts (78%) rename src/agents/{pi-embedded-runner => embedded-agent-runner}/wait-for-idle-before-flush.ts (100%) rename src/agents/{pi-embedded-subscribe.block-reply-rejections.test.ts => embedded-agent-subscribe.block-reply-rejections.test.ts} (93%) rename src/agents/{pi-embedded-subscribe.code-span-awareness.test.ts => embedded-agent-subscribe.code-span-awareness.test.ts} (89%) rename src/agents/{pi-embedded-subscribe.compaction-test-helpers.ts => embedded-agent-subscribe.compaction-test-helpers.ts} (100%) rename src/agents/{pi-embedded-subscribe.e2e-harness.ts => embedded-agent-subscribe.e2e-harness.ts} (86%) rename src/agents/{pi-embedded-subscribe.handlers.compaction.runtime.ts => embedded-agent-subscribe.handlers.compaction.runtime.ts} (100%) rename src/agents/{pi-embedded-subscribe.handlers.compaction.test.ts => embedded-agent-subscribe.handlers.compaction.test.ts} (96%) rename src/agents/{pi-embedded-subscribe.handlers.compaction.ts => embedded-agent-subscribe.handlers.compaction.ts} (92%) rename src/agents/{pi-embedded-subscribe.handlers.lifecycle.test.ts => embedded-agent-subscribe.handlers.lifecycle.test.ts} (97%) rename src/agents/{pi-embedded-subscribe.handlers.lifecycle.ts => embedded-agent-subscribe.handlers.lifecycle.ts} (92%) rename src/agents/{pi-embedded-subscribe.handlers.messages.test.ts => embedded-agent-subscribe.handlers.messages.test.ts} (98%) rename src/agents/{pi-embedded-subscribe.handlers.messages.ts => embedded-agent-subscribe.handlers.messages.ts} (95%) rename src/agents/{pi-embedded-subscribe.handlers.tools.media.test.ts => embedded-agent-subscribe.handlers.tools.media.test.ts} (86%) rename src/agents/{pi-embedded-subscribe.handlers.tools.test.ts => embedded-agent-subscribe.handlers.tools.test.ts} (99%) rename src/agents/{pi-embedded-subscribe.handlers.tools.ts => embedded-agent-subscribe.handlers.tools.ts} (98%) rename src/agents/{pi-embedded-subscribe.handlers.ts => embedded-agent-subscribe.handlers.ts} (85%) rename src/agents/{pi-embedded-subscribe.handlers.types.ts => embedded-agent-subscribe.handlers.types.ts} (90%) rename src/agents/{pi-embedded-subscribe.lifecycle-billing-error.test.ts => embedded-agent-subscribe.lifecycle-billing-error.test.ts} (90%) rename src/agents/{pi-embedded-subscribe.openai-responses.test-helpers.ts => embedded-agent-subscribe.openai-responses.test-helpers.ts} (100%) rename src/agents/{pi-embedded-subscribe.promise.ts => embedded-agent-subscribe.promise.ts} (100%) rename src/agents/{pi-embedded-subscribe.raw-stream.ts => embedded-agent-subscribe.raw-stream.ts} (100%) rename src/agents/{pi-embedded-subscribe.reply-tags.test.ts => embedded-agent-subscribe.reply-tags.test.ts} (90%) rename src/agents/{pi-embedded-subscribe.shared-types.ts => embedded-agent-subscribe.shared-types.ts} (65%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts} (91%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-append-text-end-content-is.test.ts} (98%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-call-onblockreplyflush-callback-is-not.test.ts} (82%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-duplicate-text-end-repeats-full.test.ts} (93%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-emit-duplicate-block-replies-text.test.ts} (87%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.emits-block-replies-text-end-does-not.test.ts} (97%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.emits-reasoning-as-separate-message-enabled.test.ts} (90%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.filters-final-suppresses-output-without-start-tag.test.ts} (93%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.includes-canvas-action-metadata-tool-summaries.test.ts} (93%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts} (84%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.keeps-indented-fenced-blocks-intact.test.ts} (92%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.reopens-fenced-blocks-splitting-inside-them.test.ts} (91%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.splits-long-single-line-fenced-blocks-reopen.test.ts} (85%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.streams-soft-chunks-paragraph-preference.test.ts} (93%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.subscribeembeddedagentsession.test.ts} (94%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-commentary-phase-output.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.suppresses-commentary-phase-output.test.ts} (90%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.suppresses-message-end-block-replies-message-tool.test.ts} (95%) rename src/agents/{pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts => embedded-agent-subscribe.subscribe-embedded-agent-session.waits-multiple-compaction-retries-before-resolving.test.ts} (98%) rename src/agents/{pi-embedded-subscribe.tool-text-diagnostics.ts => embedded-agent-subscribe.tool-text-diagnostics.ts} (93%) rename src/agents/{pi-embedded-subscribe.tools.extract.test.ts => embedded-agent-subscribe.tools.extract.test.ts} (98%) rename src/agents/{pi-embedded-subscribe.tools.media.test.ts => embedded-agent-subscribe.tools.media.test.ts} (98%) rename src/agents/{pi-embedded-subscribe.tools.test.ts => embedded-agent-subscribe.tools.test.ts} (99%) rename src/agents/{pi-embedded-subscribe.tools.ts => embedded-agent-subscribe.tools.ts} (98%) rename src/agents/{pi-embedded-subscribe.ts => embedded-agent-subscribe.ts} (96%) rename src/agents/{pi-embedded-subscribe.types.ts => embedded-agent-subscribe.types.ts} (89%) rename src/agents/{pi-embedded-utils.strip-model-special-tokens.test.ts => embedded-agent-utils.strip-model-special-tokens.test.ts} (92%) rename src/agents/{pi-embedded-utils.test.ts => embedded-agent-utils.test.ts} (99%) rename src/agents/{pi-embedded-utils.ts => embedded-agent-utils.ts} (98%) create mode 100644 src/agents/embedded-agent.runtime.ts rename src/agents/{pi-embedded.ts => embedded-agent.ts} (53%) create mode 100644 src/agents/harness/builtin-openclaw.ts delete mode 100644 src/agents/harness/builtin-pi.ts create mode 100644 src/agents/model-registry-loader.ts create mode 100644 src/agents/modes/interactive/components/diff.ts create mode 100644 src/agents/modes/interactive/components/keybinding-hints.ts create mode 100644 src/agents/modes/interactive/components/visual-truncate.ts create mode 100644 src/agents/modes/interactive/theme/dark.json create mode 100644 src/agents/modes/interactive/theme/light.json create mode 100644 src/agents/modes/interactive/theme/theme-schema.json create mode 100644 src/agents/modes/interactive/theme/theme.ts delete mode 100644 src/agents/pi-embedded-runner.ts delete mode 100644 src/agents/pi-embedded-runner/aliases.test.ts delete mode 100644 src/agents/pi-embedded-runner/anthropic-cache-control-payload.test.ts delete mode 100644 src/agents/pi-embedded-runner/anthropic-cache-control-payload.ts delete mode 100644 src/agents/pi-embedded-runner/anthropic-family-cache-semantics.ts delete mode 100644 src/agents/pi-embedded-runner/anthropic-family-tool-payload-compat.ts delete mode 100644 src/agents/pi-embedded-runner/compact.runtime.ts delete mode 100644 src/agents/pi-embedded-runner/compact.runtime.types.ts delete mode 100644 src/agents/pi-embedded-runner/google-stream-wrappers.test.ts delete mode 100644 src/agents/pi-embedded-runner/google-stream-wrappers.ts delete mode 100644 src/agents/pi-embedded-runner/minimax-stream-wrappers.test.ts delete mode 100644 src/agents/pi-embedded-runner/minimax-stream-wrappers.ts delete mode 100644 src/agents/pi-embedded-runner/model-context-tokens.ts delete mode 100644 src/agents/pi-embedded-runner/model.provider-normalization.ts delete mode 100644 src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts delete mode 100644 src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts delete mode 100644 src/agents/pi-embedded-runner/openai-stream-wrappers.test.ts delete mode 100644 src/agents/pi-embedded-runner/openai-stream-wrappers.ts delete mode 100644 src/agents/pi-embedded-runner/proxy-stream-wrappers.test.ts delete mode 100644 src/agents/pi-embedded-runner/proxy-stream-wrappers.ts delete mode 100644 src/agents/pi-embedded-runner/reasoning-effort-utils.test.ts delete mode 100644 src/agents/pi-embedded-runner/reasoning-effort-utils.ts delete mode 100644 src/agents/pi-embedded-runner/runtime.ts delete mode 100644 src/agents/pi-embedded-runner/stream-payload-utils.ts delete mode 100644 src/agents/pi-embedded-runner/zai-stream-wrappers.ts delete mode 100644 src/agents/pi-embedded.runtime.ts delete mode 100644 src/agents/pi-model-discovery-runtime.ts delete mode 100644 src/agents/pi-model-discovery.compat.e2e.test.ts delete mode 100644 src/agents/pi-model-discovery.ts create mode 100644 src/agents/session-runtime-compat.ts create mode 100644 src/agents/sessions/agent-session-runtime.ts create mode 100644 src/agents/sessions/agent-session-services.ts create mode 100644 src/agents/sessions/agent-session.ts create mode 100644 src/agents/sessions/auth-guidance.ts create mode 100644 src/agents/sessions/auth-storage.ts create mode 100644 src/agents/sessions/bash-executor.ts create mode 100644 src/agents/sessions/compaction/branch-summarization.ts create mode 100644 src/agents/sessions/compaction/compaction.ts create mode 100644 src/agents/sessions/compaction/index.ts create mode 100644 src/agents/sessions/compaction/utils.ts create mode 100644 src/agents/sessions/defaults.ts create mode 100644 src/agents/sessions/diagnostics.ts create mode 100644 src/agents/sessions/event-bus.ts create mode 100644 src/agents/sessions/exec.ts create mode 100644 src/agents/sessions/extension-sdk.ts create mode 100644 src/agents/sessions/extensions/index.ts create mode 100644 src/agents/sessions/extensions/loader.test.ts create mode 100644 src/agents/sessions/extensions/loader.ts create mode 100644 src/agents/sessions/extensions/runner.ts create mode 100644 src/agents/sessions/extensions/types.ts create mode 100644 src/agents/sessions/extensions/wrapper.ts create mode 100644 src/agents/sessions/footer-data-provider.ts create mode 100644 src/agents/sessions/http-dispatcher.ts create mode 100644 src/agents/sessions/index.ts create mode 100644 src/agents/sessions/keybindings.ts create mode 100644 src/agents/sessions/messages.ts create mode 100644 src/agents/sessions/model-registry.ts create mode 100644 src/agents/sessions/model-resolver.ts create mode 100644 src/agents/sessions/output-guard.ts create mode 100644 src/agents/sessions/package-manager.test.ts create mode 100644 src/agents/sessions/package-manager.ts create mode 100644 src/agents/sessions/prompt-templates.ts create mode 100644 src/agents/sessions/provider-display-names.ts create mode 100644 src/agents/sessions/resolve-config-value.ts create mode 100644 src/agents/sessions/resource-loader.ts create mode 100644 src/agents/sessions/sdk.ts create mode 100644 src/agents/sessions/session-cwd.ts create mode 100644 src/agents/sessions/session-manager.ts create mode 100644 src/agents/sessions/settings-manager.ts create mode 100644 src/agents/sessions/skills.ts create mode 100644 src/agents/sessions/slash-commands.ts create mode 100644 src/agents/sessions/source-info.ts create mode 100644 src/agents/sessions/system-prompt.ts create mode 100644 src/agents/sessions/telemetry.ts create mode 100644 src/agents/sessions/timings.ts create mode 100644 src/agents/sessions/tools/bash.ts create mode 100644 src/agents/sessions/tools/edit-diff.ts create mode 100644 src/agents/sessions/tools/edit.ts create mode 100644 src/agents/sessions/tools/file-mutation-queue.ts create mode 100644 src/agents/sessions/tools/find.ts create mode 100644 src/agents/sessions/tools/grep.ts create mode 100644 src/agents/sessions/tools/index.ts create mode 100644 src/agents/sessions/tools/ls.ts create mode 100644 src/agents/sessions/tools/output-accumulator.ts create mode 100644 src/agents/sessions/tools/path-utils.ts create mode 100644 src/agents/sessions/tools/read.ts create mode 100644 src/agents/sessions/tools/render-utils.ts create mode 100644 src/agents/sessions/tools/tool-definition-wrapper.ts create mode 100644 src/agents/sessions/tools/truncate.ts create mode 100644 src/agents/sessions/tools/write.ts rename src/agents/test-helpers/{pi-coding-agent-token-mock.ts => agent-session-token-mock.ts} (71%) rename src/agents/test-helpers/{pi-tool-stubs.ts => agent-tool-stubs.ts} (75%) rename src/agents/test-helpers/{pi-tools-fs-helpers.ts => agent-tools-fs-helpers.ts} (100%) rename src/agents/test-helpers/{pi-tools-sandbox-context.test.ts => agent-tools-sandbox-context.test.ts} (82%) rename src/agents/test-helpers/{pi-tools-sandbox-context.ts => agent-tools-sandbox-context.ts} (90%) rename src/agents/test-helpers/{pi-embedded-runner-e2e-fixtures.ts => embedded-agent-runner-e2e-fixtures.ts} (86%) rename src/agents/test-helpers/{pi-embedded-runner-e2e-mocks.ts => embedded-agent-runner-e2e-mocks.ts} (96%) create mode 100644 src/agents/utils/ansi.ts create mode 100644 src/agents/utils/changelog.ts create mode 100644 src/agents/utils/child-process.ts create mode 100644 src/agents/utils/clipboard-image.ts create mode 100644 src/agents/utils/clipboard-native.ts create mode 100644 src/agents/utils/clipboard.ts create mode 100644 src/agents/utils/exif-orientation.ts create mode 100644 src/agents/utils/frontmatter.ts create mode 100644 src/agents/utils/fs-watch.ts create mode 100644 src/agents/utils/git.test.ts create mode 100644 src/agents/utils/git.ts create mode 100644 src/agents/utils/html.ts create mode 100644 src/agents/utils/image-convert.ts create mode 100644 src/agents/utils/image-resize.ts create mode 100644 src/agents/utils/mime.ts create mode 100644 src/agents/utils/paths.ts create mode 100644 src/agents/utils/photon.ts create mode 100644 src/agents/utils/shell.ts create mode 100644 src/agents/utils/sleep.ts create mode 100644 src/agents/utils/syntax-highlight.ts create mode 100644 src/agents/utils/tools-manager.ts delete mode 100644 src/auto-reply/reply/runtime-plugins.runtime.ts delete mode 100644 src/commands/status.gateway-connection.runtime.ts create mode 100644 src/config/sessions/transcript-header.ts delete mode 100644 src/cron/isolated-agent/run-runtime-plugins.runtime.ts create mode 100644 src/gateway/server-startup.test.ts create mode 100644 src/plugin-sdk/agent-core.ts create mode 100644 src/plugin-sdk/agent-sessions.ts create mode 100644 src/plugin-sdk/provider-usage.test.ts delete mode 100644 src/plugins/pi-package-graph.test.ts create mode 100644 src/plugins/runtime-plugins.runtime.ts create mode 100644 src/plugins/runtime/runtime-embedded-agent.runtime.ts delete mode 100644 src/plugins/runtime/runtime-embedded-pi.runtime.ts rename src/types/{pi-agent-core.d.ts => agent-core.d.ts} (79%) create mode 100644 src/types/agent-sessions.d.ts create mode 100644 src/types/highlight-js-lib-index.d.ts delete mode 100644 src/types/pi-coding-agent.d.ts rename test/helpers/agents/{pi-ai-stream-simple-mock.ts => llm-stream-simple-mock.ts} (73%) create mode 100644 test/vitest/vitest.agents-embedded-agent.config.ts delete mode 100644 test/vitest/vitest.agents-pi-embedded.config.ts diff --git a/.github/codeql/codeql-agent-runtime-boundary-critical-quality.yml b/.github/codeql/codeql-agent-runtime-boundary-critical-quality.yml index 8c355f524fc..5d0df4bec02 100644 --- a/.github/codeql/codeql-agent-runtime-boundary-critical-quality.yml +++ b/.github/codeql/codeql-agent-runtime-boundary-critical-quality.yml @@ -17,7 +17,7 @@ paths: - src/acp/control-plane - src/agents/command - src/agents/cli-runner - - src/agents/pi-embedded-runner + - src/agents/embedded-agent-runner - src/agents/tools - src/agents/*completion*.ts - src/agents/*transport*.ts diff --git a/.github/codeql/codeql-mcp-process-tool-boundary-critical-security.yml b/.github/codeql/codeql-mcp-process-tool-boundary-critical-security.yml index 1a03f820290..b72485da2c9 100644 --- a/.github/codeql/codeql-mcp-process-tool-boundary-critical-security.yml +++ b/.github/codeql/codeql-mcp-process-tool-boundary-critical-security.yml @@ -24,14 +24,14 @@ paths: - src/agents/openclaw-plugin-tools.ts - src/agents/openclaw-tools.runtime.ts - src/agents/openclaw-tools.registration.ts - - src/agents/pi-tool-definition-adapter.ts - - src/agents/pi-tools.abort.ts - - src/agents/pi-tools.before-tool-call*.ts - - src/agents/pi-tools.host-edit.ts - - src/agents/pi-tools-parameter-schema.ts - - src/agents/pi-embedded-runner/effective-tool-policy.ts - - src/agents/pi-embedded-runner/tool-name-allowlist.ts - - src/agents/pi-embedded-runner/tool-schema-runtime.ts + - src/agents/agent-tool-definition-adapter.ts + - src/agents/agent-tools.abort.ts + - src/agents/agent-tools.before-tool-call*.ts + - src/agents/agent-tools.host-edit.ts + - src/agents/agent-tools-parameter-schema.ts + - src/agents/embedded-agent-runner/effective-tool-policy.ts + - src/agents/embedded-agent-runner/tool-name-allowlist.ts + - src/agents/embedded-agent-runner/tool-schema-runtime.ts - src/agents/tools/gateway-tool.ts - src/agents/tools/message-tool.ts - src/agents/tools/sessions-send-tool.ts diff --git a/.github/workflows/codeql-critical-quality.yml b/.github/workflows/codeql-critical-quality.yml index f7138158250..e2568c77ad5 100644 --- a/.github/workflows/codeql-critical-quality.yml +++ b/.github/workflows/codeql-critical-quality.yml @@ -71,7 +71,7 @@ on: - "src/acp/control-plane/**" - "src/agents/cli-runner/**" - "src/agents/command/**" - - "src/agents/pi-embedded-runner/**" + - "src/agents/embedded-agent-runner/**" - "src/agents/tools/**" - "src/agents/*completion*.ts" - "src/agents/*transport*.ts" @@ -222,7 +222,7 @@ jobs: network_runtime=true session_diagnostics=true ;; - src/acp/control-plane/*|src/agents/cli-runner/*|src/agents/command/*|src/agents/pi-embedded-runner/*|src/agents/tools/*|src/agents/*completion*.ts|src/agents/*transport*.ts|src/agents/model-*.ts|src/agents/openclaw-tools*.ts|src/agents/provider-*.ts|src/agents/session*.ts|src/agents/tool-call*.ts|src/auto-reply/reply/agent-runner*.ts|src/auto-reply/reply/commands*.ts|src/auto-reply/reply/directive-handling*.ts|src/auto-reply/reply/dispatch-*.ts|src/auto-reply/reply/get-reply-run*.ts|src/auto-reply/reply/provider-dispatcher*.ts|src/auto-reply/reply/queue*.ts|src/auto-reply/reply/reply-run-registry*.ts|src/auto-reply/reply/session*.ts) + src/acp/control-plane/*|src/agents/cli-runner/*|src/agents/command/*|src/agents/embedded-agent-runner/*|src/agents/tools/*|src/agents/*completion*.ts|src/agents/*transport*.ts|src/agents/model-*.ts|src/agents/openclaw-tools*.ts|src/agents/provider-*.ts|src/agents/session*.ts|src/agents/tool-call*.ts|src/auto-reply/reply/agent-runner*.ts|src/auto-reply/reply/commands*.ts|src/auto-reply/reply/directive-handling*.ts|src/auto-reply/reply/dispatch-*.ts|src/auto-reply/reply/get-reply-run*.ts|src/auto-reply/reply/provider-dispatcher*.ts|src/auto-reply/reply/queue*.ts|src/auto-reply/reply/reply-run-registry*.ts|src/auto-reply/reply/session*.ts) agent=true ;; src/auto-reply/reply/post-compaction-context.ts|src/auto-reply/reply/queue/*|src/auto-reply/reply/startup-context.ts|src/commands/doctor-session-*.ts|src/commands/session-store-targets.ts|src/commands/sessions*.ts|src/infra/diagnostic-*.ts|src/infra/diagnostics-timeline.ts|src/infra/session-delivery-queue*.ts|src/logging/diagnostic*.ts) diff --git a/.github/workflows/openclaw-release-checks.yml b/.github/workflows/openclaw-release-checks.yml index f1ebbc48ce2..8efdf56fed8 100644 --- a/.github/workflows/openclaw-release-checks.yml +++ b/.github/workflows/openclaw-release-checks.yml @@ -946,7 +946,7 @@ jobs: --concurrency "${QA_PARITY_CONCURRENCY}" \ --model "${OPENCLAW_CI_OPENAI_MODEL}" \ --alt-model "openai/gpt-5.5-alt" \ - --runtime-pair pi,codex \ + --runtime-pair openclaw,codex \ --output-dir ".artifacts/qa-e2e/runtime-parity" - name: Run standard runtime parity tier @@ -959,7 +959,7 @@ jobs: --concurrency "${QA_PARITY_CONCURRENCY}" \ --model "${OPENCLAW_CI_OPENAI_MODEL}" \ --alt-model "openai/gpt-5.5-alt" \ - --runtime-pair pi,codex \ + --runtime-pair openclaw,codex \ --output-dir ".artifacts/qa-e2e/runtime-parity-standard" - name: Run soak runtime parity tier @@ -973,7 +973,7 @@ jobs: --concurrency "${QA_PARITY_CONCURRENCY}" \ --model "${OPENCLAW_CI_OPENAI_MODEL}" \ --alt-model "openai/gpt-5.5-alt" \ - --runtime-pair pi,codex \ + --runtime-pair openclaw,codex \ --output-dir ".artifacts/qa-e2e/runtime-parity-soak" - name: Generate runtime parity report diff --git a/.github/workflows/qa-live-transports-convex.yml b/.github/workflows/qa-live-transports-convex.yml index d19bf4b5a0f..f70cfd2f39d 100644 --- a/.github/workflows/qa-live-transports-convex.yml +++ b/.github/workflows/qa-live-transports-convex.yml @@ -289,7 +289,7 @@ jobs: --concurrency "${QA_PARITY_CONCURRENCY}" \ --model "${OPENCLAW_CI_OPENAI_MODEL}" \ --alt-model "${OPENCLAW_CI_OPENAI_MODEL}" \ - --runtime-pair pi,codex \ + --runtime-pair openclaw,codex \ --fast \ --allow-failures \ --output-dir "${output_dir}/runtime-suite" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c35651a47c..89602507be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -233,6 +233,7 @@ Docs: https://docs.openclaw.ai - Maintainer skills: add `openclaw-landable-bug-sweep` for producing five small, reviewed, CI-green OpenClaw bugfix PRs from issue/PR sweeps. - Control UI/chat: add search and Load More pagination to the chat session picker, keeping initial session loads bounded while making older conversations reachable. (#85237) Thanks @amknight. - CLI/onboarding: start classic onboarding when bare `openclaw` runs before an authored config exists, while keeping configured installs on Crestodian. (#72343) Thanks @fuller-stack-dev. +- Agents/runtime: internalize the former Pi agent runtime into OpenClaw, remove legacy package dependencies, and keep Pi-named SDK aliases only as deprecated plugin compatibility. - Discord: allow configuring a bounded `agentComponents.ttlMs` callback registry lifetime for long-running component workflows, with per-account overrides and a 24-hour cap. (#84189) Thanks @100menotu001. - xAI/Grok: reuse xAI OAuth auth profiles for Grok `web_search`, thread active-agent auth through web search, add Grok model aliases, and let media providers declare default operation timeouts. (#85182) Thanks @fuller-stack-dev. - Plugin SDK: add row-level session workflow helpers and deprecate `loadSessionStore` so plugins can read and patch sessions without depending on the legacy whole-store shape. (#84693) Thanks @efpiva. diff --git a/LICENSE b/LICENSE index ed064819ab5..ebaebf7c416 100644 --- a/LICENSE +++ b/LICENSE @@ -19,3 +19,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Third-party notices for incorporated or adapted code are recorded in +THIRD_PARTY_NOTICES.md. diff --git a/README.md b/README.md index 1a2ca1609e5..9de37c85cbd 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ If you want a personal, single-user assistant that feels local, fast, and always Supported channels include: WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, IRC, Microsoft Teams, Matrix, Feishu, LINE, Mattermost, Nextcloud Talk, Nostr, Synology Chat, Tlon, Twitch, Zalo, Zalo Personal, WeChat, QQ, WebChat. -[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/help/faq) · [Onboarding](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd) +[Website](https://openclaw.ai) · [Docs](https://docs.openclaw.ai) · [Vision](VISION.md) · [Third-party notices](THIRD_PARTY_NOTICES.md) · [DeepWiki](https://deepwiki.com/openclaw/openclaw) · [Getting Started](https://docs.openclaw.ai/start/getting-started) · [Updating](https://docs.openclaw.ai/install/updating) · [Showcase](https://docs.openclaw.ai/start/showcase) · [FAQ](https://docs.openclaw.ai/help/faq) · [Onboarding](https://docs.openclaw.ai/start/wizard) · [Nix](https://github.com/openclaw/nix-openclaw) · [Docker](https://docs.openclaw.ai/install/docker) · [Discord](https://discord.gg/clawd) New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started) @@ -306,7 +306,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, maintainers, and how to s AI/vibe-coded PRs welcome! 🤖 Special thanks to [Mario Zechner](https://mariozechner.at/) for his support and for -[pi-mono](https://github.com/badlogic/pi-mono). +[pi-mono](https://github.com/earendil-works/pi-mono). Special thanks to Adam Doppelt for the lobster.bot domain. Thanks to all clawtributors: diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 00000000000..6b6721901b7 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,37 @@ +# Third-party notices + +This file records third-party notices for code or substantial implementation +portions incorporated into OpenClaw source, beyond normal package-manager +dependency metadata. + +## Pi / pi-mono + +Portions of OpenClaw were adapted from Pi / pi-mono, and OpenClaw also depends +on `@earendil-works/pi-tui` for terminal UI rendering. + +- Upstream: https://github.com/earendil-works/pi-mono +- Package family: `@earendil-works/pi-*` +- License: MIT +- Copyright: Copyright (c) 2025 Mario Zechner + +MIT License + +Copyright (c) 2025 Mario Zechner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/macos/Sources/OpenClaw/ModelCatalogLoader.swift b/apps/macos/Sources/OpenClaw/ModelCatalogLoader.swift index b1fd70fd171..baf0371a958 100644 --- a/apps/macos/Sources/OpenClaw/ModelCatalogLoader.swift +++ b/apps/macos/Sources/OpenClaw/ModelCatalogLoader.swift @@ -57,7 +57,7 @@ enum ModelCatalogLoader { let cache = self.cachePath.path if FileManager().isReadableFile(atPath: cache) { return cache } if let bundlePath = self.bundleCatalogPath() { return bundlePath } - if let nodePath = self.nodeModulesCatalogPath() { return nodePath } + if let sourcePath = self.sourceCatalogPath() { return sourcePath } return cache } @@ -77,9 +77,9 @@ enum ModelCatalogLoader { return (cache, false) } - if let nodePath = self.nodeModulesCatalogPath(), nodePath != preferred { - self.logger.warning("model catalog path missing; falling back to node_modules catalog") - return (nodePath, true) + if let sourcePath = self.sourceCatalogPath(), sourcePath != preferred { + self.logger.warning("model catalog path missing; falling back to source catalog") + return (sourcePath, true) } return nil @@ -92,14 +92,14 @@ enum ModelCatalogLoader { return url.path } - private static func nodeModulesCatalogPath() -> String? { + private static func sourceCatalogPath() -> String? { let roots = [ URL(fileURLWithPath: CommandResolver.projectRootPath()), URL(fileURLWithPath: FileManager().currentDirectoryPath), ] for root in roots { let candidate = root - .appendingPathComponent("node_modules/@earendil-works/pi-ai/dist/models.generated.js") + .appendingPathComponent("src/llm/models.generated.ts") if FileManager().isReadableFile(atPath: candidate.path) { return candidate.path } diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index 9d0c5fbf3f3..7464b8d570b 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -1,2 +1,2 @@ -83399e00723ea5cc4e7d3a4db0baaef73ad681a42baf72d18b088c649c1c7772 plugin-sdk-api-baseline.json -89fd85479942e9cc3bf30692a0a94a0a0ebfed72ebe9eaf36cec650103cddb11 plugin-sdk-api-baseline.jsonl +fc8b66cdae850d22bd2baee2232892c5d511c394eca2d29a20c0a562ed60882d plugin-sdk-api-baseline.json +cd314fd9ae8a6063c767e630e94ede222ff4234ae670a7bddc6d0fbb8bf0e280 plugin-sdk-api-baseline.jsonl diff --git a/docs/agent-runtime-architecture.md b/docs/agent-runtime-architecture.md new file mode 100644 index 00000000000..b3ff05c7cfe --- /dev/null +++ b/docs/agent-runtime-architecture.md @@ -0,0 +1,48 @@ +--- +title: "Agent runtime architecture" +summary: "How OpenClaw runs the built-in agent runtime, providers, sessions, tools, and extensions." +--- + +OpenClaw owns the built-in agent runtime directly. The runtime code lives under `src/agents/`, model/provider helpers live under `src/llm/`, and plugin-facing contracts are exposed through `openclaw/plugin-sdk/*` barrels. + +## Runtime Layout + +- `src/agents/embedded-agent-runner/`: built-in agent attempt loop, provider stream adapters, compaction, model selection, and session wiring. +- `src/agents/sessions/`: session persistence, extension loading, resource discovery, skills, prompts, themes, and TUI-backed tool renderers. +- `packages/agent-core/`: reusable agent core, lower-level harness types, messages, compaction helpers, prompt templates, and tool/session contracts. +- `src/agents/runtime/`: OpenClaw facade for `@openclaw/agent-core` plus local proxy utilities. +- `src/agents/agent-tools*.ts`: OpenClaw-owned tool definitions, schemas, policy, before/after hook adapters, and host edit support. +- `src/agents/agent-hooks/`: built-in runtime hooks such as compaction safeguards and context pruning. +- `src/llm/`: model/provider registry, transport helpers, and provider-specific stream implementations. + +## Boundaries + +Core code calls the built-in runtime through OpenClaw modules and SDK barrels, not through old external agent packages. Plugins use documented `openclaw/plugin-sdk/*` entrypoints and do not import `src/**` internals. + +`@earendil-works/pi-tui` remains a third-party TUI dependency. It is used as a terminal component toolkit by the local TUI and session renderers; internalizing it would be a separate vendoring effort. + +## Manifests + +Resource packages declare OpenClaw resources in package metadata: + +```json +{ + "openclaw": { + "extensions": ["extensions/index.ts"], + "skills": ["skills/*.md"], + "prompts": ["prompts/*.md"], + "themes": ["themes/*.json"] + } +} +``` + +The package manager also discovers conventional `extensions/`, `skills/`, `prompts/`, and `themes/` directories. + +## Runtime Selection + +The default built-in runtime id is `openclaw`. Plugin harnesses can register additional runtime ids. `auto` selects a supporting plugin harness when one exists and otherwise uses the built-in OpenClaw runtime. + +## Related + +- [OpenClaw agent runtime workflow](/openclaw-agent-runtime) +- [Agent runtimes](/concepts/agent-runtimes) diff --git a/docs/auth-credential-semantics.md b/docs/auth-credential-semantics.md index 2b4b22511e4..a072e63e753 100644 --- a/docs/auth-credential-semantics.md +++ b/docs/auth-credential-semantics.md @@ -66,7 +66,7 @@ the target agent signs in separately and creates its own local profile. `auth.profiles` entries with `mode: "aws-sdk"` are routing metadata, not stored credentials. They are valid when the target provider uses -`models.providers..auth: "aws-sdk"` or the built-in Amazon Bedrock default +`models.providers..auth: "aws-sdk"` or plugin-owned Amazon Bedrock setup AWS SDK route. These profile ids may appear in `auth.order` and session overrides even when no matching entry exists in `auth-profiles.json`. diff --git a/docs/channels/group-messages.md b/docs/channels/group-messages.md index e76017885ba..ac8642d77e1 100644 --- a/docs/channels/group-messages.md +++ b/docs/channels/group-messages.md @@ -22,7 +22,7 @@ Goal: let OpenClaw sit in WhatsApp groups, wake up only when pinged, and keep th - Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders). - Per-group sessions: session keys look like `agent::whatsapp:group:` so commands such as `/verbose on`, `/trace on`, or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads. - Context injection: **pending-only** group messages (default 50) that _did not_ trigger a run are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. Messages already in the session are not re-injected. -- Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so Pi knows who is speaking. +- Sender surfacing: every group batch now ends with `[from: Sender Name (+E164)]` so OpenClaw knows who is speaking. - Ephemeral/view-once: we unwrap those before extracting text/mentions, so pings inside them still trigger. - Group system prompt: on the first turn of a group session (and whenever `/activation` changes the mode) we inject a short blurb into the system prompt like `You are replying inside the WhatsApp group "". Group members: Alice (+44...), Bob (+43...), ... Activation: trigger-only ... Address the specific sender noted in the message context.` If metadata isn't available we still tell the agent it's a group chat. diff --git a/docs/cli/mcp.md b/docs/cli/mcp.md index b4bd8620bd5..c2970a8396c 100644 --- a/docs/cli/mcp.md +++ b/docs/cli/mcp.md @@ -352,7 +352,7 @@ This is the `openclaw mcp list`, `show`, `set`, and `unset` path. These commands do not expose OpenClaw over MCP. They manage OpenClaw-owned MCP server definitions under `mcp.servers` in OpenClaw config. -Those saved definitions are for runtimes that OpenClaw launches or configures later, such as embedded Pi and other runtime adapters. OpenClaw stores the definitions centrally so those runtimes do not need to keep their own duplicate MCP server lists. +Those saved definitions are for runtimes that OpenClaw launches or configures later, such as embedded OpenClaw and other runtime adapters. OpenClaw stores the definitions centrally so those runtimes do not need to keep their own duplicate MCP server lists. @@ -360,13 +360,13 @@ Those saved definitions are for runtimes that OpenClaw launches or configures la - they do not connect to the target MCP server - they do not validate whether the command, URL, or remote transport is reachable right now - runtime adapters decide which transport shapes they actually support at execution time - - embedded Pi exposes configured MCP tools in normal `coding` and `messaging` tool profiles; `minimal` still hides them, and `tools.deny: ["bundle-mcp"]` disables them explicitly + - embedded OpenClaw exposes configured MCP tools in normal `coding` and `messaging` tool profiles; `minimal` still hides them, and `tools.deny: ["bundle-mcp"]` disables them explicitly - session-scoped bundled MCP runtimes are reaped after `mcp.sessionIdleTtlMs` milliseconds of idle time (default 10 minutes; set `0` to disable) and one-shot embedded runs clean them up at run end -Runtime adapters may normalize this shared registry into the shape their downstream client expects. For example, embedded Pi consumes OpenClaw `transport` values directly, while Claude Code and Gemini receive CLI-native `type` values such as `http`, `sse`, or `stdio`. +Runtime adapters may normalize this shared registry into the shape their downstream client expects. For example, embedded OpenClaw consumes OpenClaw `transport` values directly, while Claude Code and Gemini receive CLI-native `type` values such as `http`, `sse`, or `stdio`. Codex app-server also honors an optional `codex` block on each server. This is OpenClaw projection metadata for Codex app-server threads only; it does not @@ -486,7 +486,7 @@ Sensitive values in `url` (userinfo) and `headers` are redacted in logs and stat | `headers` | Optional key-value map of HTTP headers (for example auth tokens) | | `connectionTimeoutMs` | Per-server connection timeout in ms (optional) | -OpenClaw config uses `transport: "streamable-http"` as the canonical spelling. CLI-native MCP `type: "http"` values are accepted when saved through `openclaw mcp set` and repaired by `openclaw doctor --fix` in existing config, but `transport` is what embedded Pi consumes directly. +OpenClaw config uses `transport: "streamable-http"` as the canonical spelling. CLI-native MCP `type: "http"` values are accepted when saved through `openclaw mcp set` and repaired by `openclaw doctor --fix` in existing config, but `transport` is what embedded OpenClaw consumes directly. Example: diff --git a/docs/cli/migrate.md b/docs/cli/migrate.md index 4412e85d6c1..1b2fd8668bd 100644 --- a/docs/cli/migrate.md +++ b/docs/cli/migrate.md @@ -186,7 +186,7 @@ openclaw migrate apply codex --yes --plugin google-calendar Apply calls app-server `plugin/install` for each selected eligible plugin, even if the target app-server already reports that plugin as installed and enabled. Migrated Codex plugins are usable only in sessions that select the - native Codex harness; they are not exposed to Pi, normal OpenAI provider runs, + native Codex harness; they are not exposed to OpenClaw provider runs, ACP conversation bindings, or other harnesses. ### Manual-review Codex state diff --git a/docs/cli/models.md b/docs/cli/models.md index 261a4c341f0..46d0249335c 100644 --- a/docs/cli/models.md +++ b/docs/cli/models.md @@ -37,7 +37,7 @@ overview, while `auth.oauth` is auth-store profile health only. Add `--probe` to run live auth probes against each configured provider profile. Probes are real requests (may consume tokens and trigger rate limits). Use `--agent ` to inspect a configured agent's model/auth state. When omitted, -the command uses `OPENCLAW_AGENT_DIR`/`PI_CODING_AGENT_DIR` if set, otherwise the +the command uses `OPENCLAW_AGENT_DIR` if set, otherwise the configured default agent. Probe rows can come from auth profiles, env credentials, or `models.json`. For Codex OAuth troubleshooting, `openclaw models status`, @@ -129,7 +129,7 @@ Options: - `--probe-timeout ` - `--probe-concurrency ` - `--probe-max-tokens ` -- `--agent ` (configured agent id; overrides `OPENCLAW_AGENT_DIR`/`PI_CODING_AGENT_DIR`) +- `--agent ` (configured agent id; overrides `OPENCLAW_AGENT_DIR`) `--json` keeps stdout reserved for the JSON payload. Auth-profile, provider, and startup diagnostics are routed to stderr so scripts can pipe stdout directly diff --git a/docs/cli/status.md b/docs/cli/status.md index db743436f34..3524e4ad2f0 100644 --- a/docs/cli/status.md +++ b/docs/cli/status.md @@ -21,7 +21,7 @@ Notes: - Plain `openclaw status` stays on the fast read-only path and marks memory as `not checked` instead of unavailable when it skips memory inspection. Heavy security audit, plugin compatibility, and memory-vector probes are left to `openclaw status --all`, `openclaw status --deep`, `openclaw security audit`, and `openclaw memory status --deep`. - `status --json --all` reports memory details from the active memory plugin runtime selected by `plugins.slots.memory`. Custom memory plugins can leave built-in `agents.defaults.memorySearch.enabled` disabled and still report their own files, chunks, vector, and FTS state. - `--usage` prints normalized provider usage windows as `X% left`. -- Session status output separates `Execution:` from `Runtime:`. `Execution` is the sandbox path (`direct`, `docker/*`), while `Runtime` tells you whether the session is using `OpenClaw Pi Default`, `OpenAI Codex`, a CLI backend, or an ACP backend such as `codex (acp/acpx)`. See [Agent runtimes](/concepts/agent-runtimes) for the provider/model/runtime distinction. +- Session status output separates `Execution:` from `Runtime:`. `Execution` is the sandbox path (`direct`, `docker/*`), while `Runtime` tells you whether the session is using `OpenClaw Default`, `OpenAI Codex`, a CLI backend, or an ACP backend such as `codex (acp/acpx)`. See [Agent runtimes](/concepts/agent-runtimes) for the provider/model/runtime distinction. - MiniMax's raw `usage_percent` / `usagePercent` fields are remaining quota, so OpenClaw inverts them before display; count-based fields win when present. `model_remains` responses prefer the chat-model entry, derive the window label from timestamps when needed, and include the model name in the plan label. - When the current session snapshot is sparse, `/status` can backfill token and cache counters from the most recent transcript usage log. Existing nonzero live values still win over transcript fallback values. - `/status` includes compact Gateway process uptime and host system uptime. diff --git a/docs/concepts/agent-loop.md b/docs/concepts/agent-loop.md index 88878b35565..8bc0ed38ec8 100644 --- a/docs/concepts/agent-loop.md +++ b/docs/concepts/agent-loop.md @@ -25,16 +25,16 @@ wired end-to-end. 2. `agentCommand` runs the agent: - resolves model + thinking/verbose/trace defaults - loads skills snapshot - - calls `runEmbeddedPiAgent` (pi-agent-core runtime) + - calls `runEmbeddedAgent` (OpenClaw agent runtime) - emits **lifecycle end/error** if the embedded loop does not emit one -3. `runEmbeddedPiAgent`: +3. `runEmbeddedAgent`: - serializes runs via per-session + global queues - - resolves model + auth profile and builds the pi session - - subscribes to pi events and streams assistant/tool deltas + - resolves model + auth profile and builds the OpenClaw session + - subscribes to runtime events and streams assistant/tool deltas - enforces timeout -> aborts run if exceeded - for Codex app-server turns, aborts an accepted turn that stops producing app-server progress before a terminal event - returns payloads + usage metadata -4. `subscribeEmbeddedPiSession` bridges pi-agent-core events to OpenClaw `agent` stream: +4. `subscribeEmbeddedAgentSession` bridges agent runtime events to OpenClaw `agent` stream: - tool events => `stream: "tool"` - assistant deltas => `stream: "assistant"` - lifecycle events => `stream: "lifecycle"` (`phase: "start" | "end" | "error"`) @@ -120,7 +120,7 @@ surfaces, while Codex native hooks remain a separate lower-level Codex mechanism ## Streaming + partial replies -- Assistant deltas are streamed from pi-agent-core and emitted as `assistant` events. +- Assistant deltas are streamed from the agent runtime and emitted as `assistant` events. - Block streaming can emit partial replies either on `text_end` or `message_end`. - Reasoning streaming can be emitted as a separate stream or as block replies. - See [Streaming](/concepts/streaming) for chunking and block reply behavior. @@ -151,9 +151,9 @@ surfaces, while Codex native hooks remain a separate lower-level Codex mechanism ## Event streams (today) -- `lifecycle`: emitted by `subscribeEmbeddedPiSession` (and as a fallback by `agentCommand`) -- `assistant`: streamed deltas from pi-agent-core -- `tool`: streamed tool events from pi-agent-core +- `lifecycle`: emitted by `subscribeEmbeddedAgentSession` (and as a fallback by `agentCommand`) +- `assistant`: streamed deltas from the agent runtime +- `tool`: streamed tool events from the agent runtime ## Chat channel handling @@ -163,7 +163,7 @@ surfaces, while Codex native hooks remain a separate lower-level Codex mechanism ## Timeouts - `agent.wait` default: 30s (just the wait). `timeoutMs` param overrides. -- Agent runtime: `agents.defaults.timeoutSeconds` default 172800s (48 hours); enforced in `runEmbeddedPiAgent` abort timer. +- Agent runtime: `agents.defaults.timeoutSeconds` default 172800s (48 hours); enforced in `runEmbeddedAgent` abort timer. - Cron runtime: isolated agent-turn `timeoutSeconds` is owned by cron. The scheduler starts that timer when execution begins, aborts the underlying run at the configured deadline, then runs bounded cleanup before recording the timeout so a stale child session cannot keep the lane stuck. - Session liveness diagnostics: with diagnostics enabled, `diagnostics.stuckSessionWarnMs` classifies long `processing` sessions that have no observed reply, tool, status, block, or ACP progress. Active embedded runs, model calls, and tool calls report as `session.long_running`; active work with no recent progress reports as `session.stalled`; `session.stuck` is reserved for recoverable stale session bookkeeping, including idle queued sessions with stale ownerless model/tool activity. Stale session bookkeeping releases the affected session lane immediately after recovery gates pass; stalled embedded runs are abort-drained only after `diagnostics.stuckSessionAbortMs` (default: at least 5 minutes and 3x the warning threshold) so queued work can resume without cutting off merely slow runs. Recovery emits structured requested/completed outcomes, and diagnostic state is marked idle only if the same processing generation is still current. Repeated `session.stuck` diagnostics back off while the session remains unchanged. - Model idle timeout: OpenClaw aborts a model request when no response chunks arrive before the idle window. `models.providers..timeoutSeconds` extends this idle watchdog for slow local/self-hosted providers, but it is still bounded by any lower `agents.defaults.timeoutSeconds` or run-specific timeout because those control the whole agent run. Otherwise OpenClaw uses `agents.defaults.timeoutSeconds` when configured, capped at 120s by default. Cron-triggered runs with no explicit model or agent timeout disable the idle watchdog and rely on the cron outer timeout. diff --git a/docs/concepts/agent-runtimes.md b/docs/concepts/agent-runtimes.md index bd12cafac93..a80ac65772a 100644 --- a/docs/concepts/agent-runtimes.md +++ b/docs/concepts/agent-runtimes.md @@ -2,7 +2,7 @@ summary: "How OpenClaw separates model providers, models, channels, and agent runtimes" title: "Agent runtimes" read_when: - - You are choosing between PI, Codex, ACP, or another native agent runtime + - You are choosing between OpenClaw, Codex, ACP, or another native agent runtime - You are confused by provider/model/runtime labels in status or config - You are documenting support parity for a native harness --- @@ -18,7 +18,7 @@ configuration. They are different layers: | ------------- | ------------------------------------- | ------------------------------------------------------------------- | | Provider | `openai`, `anthropic`, `openai-codex` | How OpenClaw authenticates, discovers models, and names model refs. | | Model | `gpt-5.5`, `claude-opus-4-6` | The model selected for the agent turn. | -| Agent runtime | `pi`, `codex`, `claude-cli` | The low level loop or backend that executes the prepared turn. | +| Agent runtime | `openclaw`, `codex`, `claude-cli` | The low level loop or backend that executes the prepared turn. | | Channel | Telegram, Discord, Slack, WhatsApp | Where messages enter and leave OpenClaw. | You will also see the word **harness** in code. A harness is the implementation @@ -32,7 +32,7 @@ runtime policy where needed. There are two runtime families: - **Embedded harnesses** run inside OpenClaw's prepared agent loop. Today this - is the built-in `pi` runtime plus registered plugin harnesses such as + is the built-in `openclaw` runtime plus registered plugin harnesses such as `codex`. - **CLI backends** run a local CLI process while keeping the model ref canonical. For example, `anthropic/claude-opus-4-7` with @@ -89,10 +89,10 @@ This is the agent-facing decision tree: native `/codex` command surface when the bundled `codex` plugin is enabled. 2. If the user asks for **Codex as the embedded runtime** or wants the normal subscription-backed Codex agent experience, use `openai/`. -3. If the user explicitly chooses **PI for an OpenAI model**, keep the model ref +3. If the user explicitly chooses **OpenClaw for an OpenAI model**, keep the model ref as `openai/` and set provider/model runtime policy to - `agentRuntime.id: "pi"`. A selected `openai-codex` auth profile is routed - internally through PI's legacy Codex-auth transport. + `agentRuntime.id: "openclaw"`. A selected `openai-codex` auth profile is routed + internally through OpenClaw's Codex-auth transport. 4. If legacy config still contains **`openai-codex/*` model refs**, repair it to `openai/` with `openclaw doctor --fix`; doctor keeps the Codex auth route by adding provider/model-scoped `agentRuntime.id: "codex"` where the @@ -119,15 +119,15 @@ contract, see [Codex harness runtime](/plugins/codex-harness-runtime#v1-support- Different runtimes own different amounts of the loop. -| Surface | OpenClaw PI embedded | Codex app-server | -| --------------------------- | --------------------------------------- | --------------------------------------------------------------------------- | -| Model loop owner | OpenClaw through the PI embedded runner | Codex app-server | -| Canonical thread state | OpenClaw transcript | Codex thread, plus OpenClaw transcript mirror | -| OpenClaw dynamic tools | Native OpenClaw tool loop | Bridged through the Codex adapter | -| Native shell and file tools | PI/OpenClaw path | Codex-native tools, bridged through native hooks where supported | -| Context engine | Native OpenClaw context assembly | OpenClaw projects assembled context into the Codex turn | -| Compaction | OpenClaw or selected context engine | Codex-native compaction, with OpenClaw notifications and mirror maintenance | -| Channel delivery | OpenClaw | OpenClaw | +| Surface | OpenClaw embedded | Codex app-server | +| --------------------------- | --------------------------------------------- | --------------------------------------------------------------------------- | +| Model loop owner | OpenClaw through the OpenClaw embedded runner | Codex app-server | +| Canonical thread state | OpenClaw transcript | Codex thread, plus OpenClaw transcript mirror | +| OpenClaw dynamic tools | Native OpenClaw tool loop | Bridged through the Codex adapter | +| Native shell and file tools | OpenClaw path | Codex-native tools, bridged through native hooks where supported | +| Context engine | Native OpenClaw context assembly | OpenClaw projects assembled context into the Codex turn | +| Compaction | OpenClaw or selected context engine | Codex-native compaction, with OpenClaw notifications and mirror maintenance | +| Channel delivery | OpenClaw | OpenClaw | This ownership split is the main design rule: @@ -149,7 +149,7 @@ OpenClaw chooses an embedded runtime after provider and model resolution: `models.providers..agentRuntime`. 3. In `auto` mode, registered plugin runtimes can claim supported provider/model pairs. -4. If no runtime claims a turn in `auto` mode, OpenClaw uses PI as the +4. If no runtime claims a turn in `auto` mode, OpenClaw uses `openclaw` as the compatibility runtime. Use an explicit runtime id when the run must be strict. @@ -161,7 +161,7 @@ legacy runtime model refs where OpenClaw can preserve the intent. Explicit provider/model plugin runtimes fail closed. For example, `agentRuntime.id: "codex"` on a provider or model means Codex or a clear -selection/runtime error; it is never silently routed back to PI. +selection/runtime error; it is never silently routed back to OpenClaw. CLI backend aliases are different from embedded harness ids. The preferred Claude CLI form is: @@ -191,10 +191,10 @@ backend. `auto` mode is intentionally conservative for most providers. OpenAI agent models are the exception: unset runtime and `auto` both resolve to the Codex -harness. Explicit PI runtime config remains an opt-in compatibility route for +harness. Explicit OpenClaw runtime config remains an opt-in compatibility route for `openai/*` agent turns; when paired with a selected `openai-codex` auth profile, -OpenClaw routes PI internally through the legacy Codex-auth transport while -keeping the public model ref as `openai/*`. Stale OpenAI PI session pins are +OpenClaw routes that path internally through the Codex-auth transport while +keeping the public model ref as `openai/*`. Stale OpenAI runtime session pins are ignored by runtime selection and can be cleaned with `openclaw doctor --fix`. If `openclaw doctor` warns that the `codex` plugin is enabled while @@ -203,7 +203,7 @@ If `openclaw doctor` warns that the `codex` plugin is enabled while ## Compatibility contract -When a runtime is not PI, it should document what OpenClaw surfaces it supports. +When a runtime is not OpenClaw, it should document what OpenClaw surfaces it supports. Use this shape for runtime docs: | Question | Why it matters | @@ -215,7 +215,7 @@ Use this shape for runtime docs: | Do native tool hooks work? | Shell, patch, and runtime-owned tools need native hook support for policy and observation. | | Does the context engine lifecycle run? | Memory and context plugins depend on assemble, ingest, after-turn, and compaction lifecycle. | | What compaction data is exposed? | Some plugins only need notifications, while others need kept/dropped metadata. | -| What is intentionally unsupported? | Users should not assume PI equivalence where the native runtime owns more state. | +| What is intentionally unsupported? | Users should not assume OpenClaw equivalence where the native runtime owns more state. | The Codex runtime support contract is documented in [Codex harness runtime](/plugins/codex-harness-runtime#v1-support-contract). diff --git a/docs/concepts/agent.md b/docs/concepts/agent.md index 02f4327d82e..af3dcf23e2b 100644 --- a/docs/concepts/agent.md +++ b/docs/concepts/agent.md @@ -69,9 +69,9 @@ Skills can be gated by config/env (see `skills` in [Gateway configuration](/gate ## Runtime boundaries -The embedded agent runtime is built on the Pi agent core (models, tools, and -prompt pipeline). Session management, discovery, tool wiring, and channel -delivery are OpenClaw-owned layers on top of that core. +The embedded agent runtime is OpenClaw-owned: model discovery, tool wiring, +prompt assembly, session management, and channel delivery share one integrated +runtime surface. ## Sessions diff --git a/docs/concepts/compaction.md b/docs/concepts/compaction.md index 5975147f28f..d2a7decbf29 100644 --- a/docs/concepts/compaction.md +++ b/docs/concepts/compaction.md @@ -54,7 +54,7 @@ Type `/compact` in any chat to force a compaction. Add instructions to guide the /compact Focus on the API design decisions ``` -When `agents.defaults.compaction.keepRecentTokens` is set, manual compaction honors that Pi cut-point and keeps the recent tail in rebuilt context. Without an explicit keep budget, manual compaction behaves as a hard checkpoint and continues from the new summary alone. +When `agents.defaults.compaction.keepRecentTokens` is set, manual compaction honors that OpenClaw cut-point and keeps the recent tail in rebuilt context. Without an explicit keep budget, manual compaction behaves as a hard checkpoint and continues from the new summary alone. ## Configuration diff --git a/docs/concepts/context-engine.md b/docs/concepts/context-engine.md index dda08773012..9c95165330e 100644 --- a/docs/concepts/context-engine.md +++ b/docs/concepts/context-engine.md @@ -241,26 +241,26 @@ info: { "agent-run": { requiredCapabilities: ["assemble-before-prompt"], unsupportedMessage: - "Use the native Codex or Pi embedded runtime, or select the legacy context engine.", + "Use the native Codex or OpenClaw embedded runtime, or select the legacy context engine.", }, }, } ``` -Native Codex and Pi embedded agent runs satisfy `assemble-before-prompt`. +Native Codex and OpenClaw embedded agent runs satisfy `assemble-before-prompt`. Generic CLI backends do not, so engines that require it are rejected before the CLI process starts. ### ownsCompaction -`ownsCompaction` controls whether Pi's built-in in-attempt auto-compaction stays enabled for the run: +`ownsCompaction` controls whether OpenClaw runtime's built-in in-attempt auto-compaction stays enabled for the run: - The engine owns compaction behavior. OpenClaw disables Pi's built-in auto-compaction for that run, and the engine's `compact()` implementation is responsible for `/compact`, overflow recovery compaction, and any proactive compaction it wants to do in `afterTurn()`. OpenClaw may still run the pre-prompt overflow safeguard; when it predicts the full transcript will overflow, the recovery path calls the active engine's `compact()` before submitting another prompt. + The engine owns compaction behavior. OpenClaw disables OpenClaw runtime's built-in auto-compaction for that run, and the engine's `compact()` implementation is responsible for `/compact`, overflow recovery compaction, and any proactive compaction it wants to do in `afterTurn()`. OpenClaw may still run the pre-prompt overflow safeguard; when it predicts the full transcript will overflow, the recovery path calls the active engine's `compact()` before submitting another prompt. - Pi's built-in auto-compaction may still run during prompt execution, but the active engine's `compact()` method is still called for `/compact` and overflow recovery. + OpenClaw runtime's built-in auto-compaction may still run during prompt execution, but the active engine's `compact()` method is still called for `/compact` and overflow recovery. diff --git a/docs/concepts/model-failover.md b/docs/concepts/model-failover.md index 51f0ed65e79..bfec2096d4c 100644 --- a/docs/concepts/model-failover.md +++ b/docs/concepts/model-failover.md @@ -184,7 +184,7 @@ When a profile fails due to auth/rate-limit errors (or a timeout that looks like Format/invalid-request errors are usually terminal because retrying the same payload would fail the same way, so OpenClaw surfaces them instead of rotating auth profiles. Known retry-repair paths can opt in explicitly: for example Cloud Code Assist tool call ID validation failures are sanitized and retried once through the `allowFormatRetry` policy. OpenAI-compatible stop-reason errors such as `Unhandled stop reason: error`, `stop reason: error`, and `reason: error` are classified as timeout/failover signals. - Generic server text can also land in that timeout bucket when the source matches a known transient pattern. For example, the bare pi-ai stream-wrapper message `An unknown error occurred` is treated as failover-worthy for every provider because pi-ai emits it when provider streams end with `stopReason: "aborted"` or `stopReason: "error"` without specific details. JSON `api_error` payloads with transient server text such as `internal server error`, `unknown error, 520`, `upstream error`, or `backend error` are also treated as failover-worthy timeouts. + Generic server text can also land in that timeout bucket when the source matches a known transient pattern. For example, the bare model runtime stream-wrapper message `An unknown error occurred` is treated as failover-worthy for every provider because the shared model runtime emits it when provider streams end with `stopReason: "aborted"` or `stopReason: "error"` without specific details. JSON `api_error` payloads with transient server text such as `internal server error`, `unknown error, 520`, `upstream error`, or `backend error` are also treated as failover-worthy timeouts. OpenRouter-specific generic upstream text such as bare `Provider returned error` is treated as timeout only when the provider context is actually OpenRouter. Generic internal fallback text such as `LLM request failed with an unknown error.` stays conservative and does not trigger failover by itself. diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 25f1b798aac..b0b9c154244 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -31,13 +31,13 @@ Reference for **LLM/model providers** (not chat channels like WhatsApp/Telegram) - `openai/` uses the native Codex app-server harness for agent turns by default. This is the usual ChatGPT/Codex subscription setup. - `openai-codex/` is legacy config that doctor rewrites to `openai/`. - - `openai/` plus provider/model `agentRuntime.id: "pi"` uses PI for explicit API-key or compatibility routes. + - `openai/` plus provider/model `agentRuntime.id: "openclaw"` uses OpenClaw's built-in runtime for explicit API-key or compatibility routes. See [OpenAI](/providers/openai) and [Codex harness](/plugins/codex-harness). If the provider/runtime split is confusing, read [Agent runtimes](/concepts/agent-runtimes) first. Plugin auto-enable follows the same boundary: `openai/*` agent refs enable the Codex plugin for the default route, and explicit provider/model `agentRuntime.id: "codex"` or legacy `codex/` refs also require it. - GPT-5.5 is available through the native Codex app-server harness by default on `openai/gpt-5.5`, and through PI only when provider/model runtime policy explicitly selects `pi`. + GPT-5.5 is available through the native Codex app-server harness by default on `openai/gpt-5.5`, and through the OpenClaw runtime when provider/model runtime policy explicitly selects `openclaw`. @@ -80,9 +80,9 @@ Provider-owned runner behavior lives on explicit provider hooks such as replay p -## Built-in providers (pi-ai catalog) +## Built-in providers -OpenClaw ships with the pi-ai catalog. These providers require **no** `models.providers` config; just set auth + pick a model. +OpenClaw ships with a built-in model catalog. These providers require **no** `models.providers` config; just set auth + pick a model. ### OpenAI @@ -92,14 +92,14 @@ OpenClaw ships with the pi-ai catalog. These providers require **no** `models.pr - Example models: `openai/gpt-5.5`, `openai/gpt-5.4-mini` - Verify account/model availability with `openclaw models list --provider openai` if a specific install or API key behaves differently. - CLI: `openclaw onboard --auth-choice openai-api-key` -- Default transport is `auto`; OpenClaw passes the transport choice to pi-ai. +- Default transport is `auto`; OpenClaw passes the transport choice to the shared model runtime. - Override per model via `agents.defaults.models["openai/"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`) - OpenAI priority processing can be enabled via `agents.defaults.models["openai/"].params.serviceTier` - `/fast` and `params.fastMode` map direct `openai/*` Responses requests to `service_tier=priority` on `api.openai.com` - Use `params.serviceTier` when you want an explicit tier instead of the shared `/fast` toggle - Hidden OpenClaw attribution headers (`originator`, `version`, `User-Agent`) apply only on native OpenAI traffic to `api.openai.com`, not generic OpenAI-compatible proxies - Native OpenAI routes also keep Responses `store`, prompt-cache hints, and OpenAI reasoning-compat payload shaping; proxy routes do not -- `openai/gpt-5.3-codex-spark` is intentionally suppressed in OpenClaw because live OpenAI API requests reject it; use `openai-codex/gpt-5.3-codex-spark` only when the Codex catalog exposes it for your account +- `openai/gpt-5.3-codex-spark` is intentionally suppressed in OpenClaw because live OpenAI API requests reject it and the current Codex catalog does not expose it ```json5 { @@ -134,23 +134,22 @@ Anthropic staff told us OpenClaw-style Claude CLI usage is allowed again, so Ope - Provider: `openai-codex` - Auth: OAuth (ChatGPT) -- Legacy PI model ref: `openai-codex/gpt-5.5` +- Legacy OpenAI Codex model ref: `openai-codex/gpt-5.5` - Native Codex app-server harness ref: `openai/gpt-5.5` - Native Codex app-server harness docs: [Codex harness](/plugins/codex-harness) - Legacy model refs: `codex/gpt-*` - Plugin boundary: `openai-codex/*` loads the OpenAI plugin; the native Codex app-server plugin is selected only by the Codex harness runtime or legacy `codex/*` refs. - CLI: `openclaw onboard --auth-choice openai-codex` or `openclaw models auth login --provider openai-codex` - Default transport is `auto` (WebSocket-first, SSE fallback) -- Override per PI model via `agents.defaults.models["openai-codex/"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`) +- Override per OpenAI Codex model via `agents.defaults.models["openai-codex/"].params.transport` (`"sse"`, `"websocket"`, or `"auto"`) - `params.serviceTier` is also forwarded on native Codex Responses requests (`chatgpt.com/backend-api`) - Hidden OpenClaw attribution headers (`originator`, `version`, `User-Agent`) are only attached on native Codex traffic to `chatgpt.com/backend-api`, not generic OpenAI-compatible proxies - Shares the same `/fast` toggle and `params.fastMode` config as direct `openai/*`; OpenClaw maps that to `service_tier=priority` - `openai-codex/gpt-5.5` uses the Codex catalog native `contextWindow = 400000` and default runtime `contextTokens = 272000`; override the runtime cap with `models.providers.openai-codex.models[].contextTokens` - Policy note: OpenAI Codex OAuth is explicitly supported for external tools/workflows like OpenClaw. - For the common subscription plus native Codex runtime route, sign in with `openai-codex` auth but configure `openai/gpt-5.5`; OpenAI agent turns select Codex by default. -- Use provider/model `agentRuntime.id: "pi"` only when you want a compatibility route through PI; otherwise keep `openai/gpt-5.5` on the default Codex harness. -- `openai-codex/gpt-*` refs remain a legacy PI route. Prefer `openai/gpt-5.5` on the native Codex runtime for new agent config, and run `openclaw doctor --fix` when you want to migrate old `openai-codex/*` refs to canonical `openai/*` refs. -- `openai-codex/gpt-5.3-codex-spark` remains available only through Codex catalog discovery when the signed-in account advertises it; direct `openai/*` and Azure refs for that model stay suppressed. +- Use provider/model `agentRuntime.id: "openclaw"` only when you want the built-in OpenClaw route; otherwise keep `openai/gpt-5.5` on the default Codex harness. +- `openai-codex/gpt-*` refs remain a legacy OpenAI Codex route. Prefer `openai/gpt-5.5` on the native Codex runtime for new agent config, and run `openclaw doctor --fix` when you want to migrate old `openai-codex/*` refs to canonical `openai/*` refs. ```json5 { diff --git a/docs/concepts/models.md b/docs/concepts/models.md index fe75ae73636..5f4cfcab7e4 100644 --- a/docs/concepts/models.md +++ b/docs/concepts/models.md @@ -16,7 +16,7 @@ sidebarTitle: "Models CLI" Quick provider overview and examples. - PI, Codex, and other agent loop runtimes. + OpenClaw, Codex, and other agent loop runtimes. Model config keys. @@ -361,7 +361,7 @@ Marker persistence is source-authoritative: OpenClaw writes markers from the act ## Related -- [Agent runtimes](/concepts/agent-runtimes) — PI, Codex, and other agent loop runtimes +- [Agent runtimes](/concepts/agent-runtimes) — OpenClaw, Codex, and other agent loop runtimes - [Configuration reference](/gateway/config-agents#agent-defaults) — model config keys - [Image generation](/tools/image-generation) — image model configuration - [Model failover](/concepts/model-failover) — fallback chains diff --git a/docs/concepts/oauth.md b/docs/concepts/oauth.md index 0d8b1b684ec..8a3c261ce28 100644 --- a/docs/concepts/oauth.md +++ b/docs/concepts/oauth.md @@ -107,7 +107,7 @@ Claude login on the host, onboarding/configure can reuse it directly. ## OAuth exchange (how login works) -OpenClaw's interactive login flows are implemented in `@earendil-works/pi-ai` and wired into the wizards/commands. +OpenClaw's interactive login flows are implemented in `openclaw/plugin-sdk/llm` and wired into the wizards/commands. ### Anthropic setup-token diff --git a/docs/concepts/qa-e2e-automation.md b/docs/concepts/qa-e2e-automation.md index a0708f36840..9223da7dfe3 100644 --- a/docs/concepts/qa-e2e-automation.md +++ b/docs/concepts/qa-e2e-automation.md @@ -34,7 +34,7 @@ script aliases; both forms are supported. | `qa run` | Bundled QA self-check; writes a Markdown report. | | `qa suite` | Run repo-backed scenarios against the QA gateway lane. Aliases: `pnpm openclaw qa suite --runner multipass` for a disposable Linux VM. | | `qa coverage` | Print the markdown scenario-coverage inventory (`--json` for machine output). | -| `qa parity-report` | Compare two `qa-suite-summary.json` files and write the agentic parity report, or use `--runtime-axis --token-efficiency` to write Codex-vs-Pi runtime parity and token-efficiency reports from one runtime-pair summary. | +| `qa parity-report` | Compare two `qa-suite-summary.json` files and write the agentic parity report, or use `--runtime-axis --token-efficiency` to write Codex-vs-OpenClaw runtime parity and token-efficiency reports from one runtime-pair summary. | | `qa character-eval` | Run the character QA scenario across multiple live models with a judged report. See [Reporting](#reporting). | | `qa manual` | Run a one-off prompt against the selected provider/model lane. | | `qa ui` | Start the QA debugger UI and local QA bus (alias: `pnpm qa:lab:ui`). | diff --git a/docs/concepts/queue-steering.md b/docs/concepts/queue-steering.md index e659b1319c5..c837b6bf10d 100644 --- a/docs/concepts/queue-steering.md +++ b/docs/concepts/queue-steering.md @@ -10,24 +10,24 @@ title: "Steering queue" When a normal prompt arrives while a session run is already streaming, OpenClaw tries to send that prompt into the active runtime by default when the queue mode is `steer`. No config entry and no queue directive are required for that default -behavior. Pi and the native Codex app-server harness implement the delivery +behavior. OpenClaw and the native Codex app-server harness implement the delivery details differently. ## Runtime boundary -Steering does not interrupt a tool call that is already running. Pi checks for +Steering does not interrupt a tool call that is already running. OpenClaw checks for queued steering messages at model boundaries: 1. The assistant asks for tool calls. -2. Pi executes the current assistant message's tool-call batch. -3. Pi emits the turn end event. -4. Pi drains queued steering messages. -5. Pi appends those messages as user messages before the next LLM call. +2. OpenClaw executes the current assistant message's tool-call batch. +3. OpenClaw emits the turn end event. +4. OpenClaw drains queued steering messages. +5. OpenClaw appends those messages as user messages before the next LLM call. This keeps tool results paired with the assistant message that requested them, then lets the next model call see the latest user input. -The native Codex app-server harness exposes `turn/steer` instead of Pi's +The native Codex app-server harness exposes `turn/steer` instead of OpenClaw runtime's internal steering queue. OpenClaw batches queued prompts for the configured quiet window, then sends a single `turn/steer` request with all collected user input in arrival order. @@ -55,7 +55,7 @@ this steering path; they wait until the active run finishes. For the explicit If four users send messages while the agent is executing a tool call: - With default behavior, the active runtime receives all four messages in - arrival order before its next model decision. Pi drains them at the next model + arrival order before its next model decision. OpenClaw drains them at the next model boundary; Codex receives them as one batched `turn/steer`. - With `/queue collect`, OpenClaw does not steer. It waits until the active run ends, then creates a followup turn with compatible queued messages after the @@ -78,8 +78,8 @@ replace the active run. `messages.queue.debounceMs` applies to queued `followup` and `collect` delivery. In `steer` mode with the native Codex harness, it also sets the quiet window -before sending batched `turn/steer`. For Pi, active steering itself does not use -the debounce timer because Pi naturally batches messages until the next model +before sending batched `turn/steer`. For OpenClaw, active steering itself does not use +the debounce timer because OpenClaw naturally batches messages until the next model boundary. ## Related diff --git a/docs/concepts/queue.md b/docs/concepts/queue.md index df316508794..1f494616de1 100644 --- a/docs/concepts/queue.md +++ b/docs/concepts/queue.md @@ -16,7 +16,7 @@ We serialize inbound auto-reply runs (all channels) through a tiny in-process qu ## How it works - A lane-aware FIFO queue drains each lane with a configurable concurrency cap (default 1 for unconfigured lanes; main defaults to 4, subagent to 8). -- `runEmbeddedPiAgent` enqueues by **session key** (lane `session:`) to guarantee only one active run per session. +- `runEmbeddedAgent` enqueues by **session key** (lane `session:`) to guarantee only one active run per session. - Each session run is then queued into a **global lane** (`main` by default) so overall parallelism is capped by `agents.defaults.maxConcurrent`. - When verbose logging is enabled, queued runs emit a short notice if they waited more than ~2s before starting. - Typing indicators still fire immediately on enqueue (when supported by the channel) so user experience is unchanged while we wait our turn. @@ -40,7 +40,7 @@ active run to finish before starting the prompt. `/queue` controls what normal inbound messages do while a session already has an active run: -- `steer`: inject messages into the active runtime. Pi delivers all pending steering messages **after the current assistant turn finishes executing its tool calls**, before the next LLM call; Codex app-server receives one batched `turn/steer`. If the run is not actively streaming or steering is unavailable, OpenClaw waits until the active run ends before starting the prompt. +- `steer`: inject messages into the active runtime. OpenClaw delivers all pending steering messages **after the current assistant turn finishes executing its tool calls**, before the next LLM call; Codex app-server receives one batched `turn/steer`. If the run is not actively streaming or steering is unavailable, OpenClaw waits until the active run ends before starting the prompt. - `followup`: do not steer. Enqueue each message for a later agent turn after the current run ends. - `collect`: do not steer. Coalesce queued messages into a **single** followup turn after the quiet window. If messages target different channels/threads, they drain individually to preserve routing. - `interrupt`: abort the active run for that session, then run the newest message. diff --git a/docs/concepts/system-prompt.md b/docs/concepts/system-prompt.md index fe86219f9b1..3199de0536f 100644 --- a/docs/concepts/system-prompt.md +++ b/docs/concepts/system-prompt.md @@ -6,7 +6,7 @@ read_when: title: "System prompt" --- -OpenClaw builds a custom system prompt for every agent run. The prompt is **OpenClaw-owned** and does not use the pi-coding-agent default prompt. +OpenClaw builds a custom system prompt for every agent run. The prompt is **OpenClaw-owned** and does not use a runtime default prompt. The prompt is assembled by OpenClaw and injected into each agent run. diff --git a/docs/docs.json b/docs/docs.json index d4d25ec1105..720bd980e10 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -72,6 +72,14 @@ "source": "/mcp", "destination": "/cli/mcp" }, + { + "source": "/pi", + "destination": "/agent-runtime-architecture" + }, + { + "source": "/pi-dev", + "destination": "/openclaw-agent-runtime" + }, { "source": "/providers/modelstudio", "destination": "/providers/qwen" @@ -1050,7 +1058,7 @@ }, { "group": "Advanced setup", - "pages": ["start/setup", "pi-dev"] + "pages": ["start/setup", "openclaw-agent-runtime"] } ] }, @@ -1766,7 +1774,7 @@ { "group": "Technical reference", "pages": [ - "pi", + "agent-runtime-architecture", "reference/wizard", "reference/token-use", "reference/secretref-credential-surface", diff --git a/docs/gateway/background-process.md b/docs/gateway/background-process.md index b0cd85e42dc..dac5f99082f 100644 --- a/docs/gateway/background-process.md +++ b/docs/gateway/background-process.md @@ -42,10 +42,10 @@ When spawning long-running child processes outside the exec/process tools (for e Environment overrides: -- `PI_BASH_YIELD_MS`: default yield (ms) -- `PI_BASH_MAX_OUTPUT_CHARS`: in-memory output cap (chars) +- `OPENCLAW_BASH_YIELD_MS`: default yield (ms) +- `OPENCLAW_BASH_MAX_OUTPUT_CHARS`: in-memory output cap (chars) - `OPENCLAW_BASH_PENDING_MAX_OUTPUT_CHARS`: pending stdout/stderr cap per stream (chars) -- `PI_BASH_JOB_TTL_MS`: TTL for finished sessions (ms, bounded to 1m–3h) +- `OPENCLAW_BASH_JOB_TTL_MS`: TTL for finished sessions (ms, bounded to 1m–3h) - `OPENCLAW_PROCESS_INPUT_WAIT_IDLE_MS`: idle-output threshold before writable background sessions are marked as likely waiting for input (default 15000 ms) Config (preferred): diff --git a/docs/gateway/config-agents.md b/docs/gateway/config-agents.md index eeefc67b794..3be0f38ea3d 100644 --- a/docs/gateway/config-agents.md +++ b/docs/gateway/config-agents.md @@ -577,7 +577,7 @@ Replace the entire OpenClaw-assembled system prompt with a fixed string. Set at ### `agents.defaults.promptOverlays` -Provider-independent prompt overlays applied by model family on OpenClaw-assembled prompt surfaces. GPT-5-family model ids receive the shared behavior contract across PI/provider routes; `personality` controls only the friendly interaction-style layer. Native Codex app-server routes keep Codex-owned base/model instructions instead of this OpenClaw GPT-5 overlay, and OpenClaw disables Codex's built-in personality for native threads. +Provider-independent prompt overlays applied by model family on OpenClaw-assembled prompt surfaces. GPT-5-family model ids receive the shared behavior contract across OpenClaw/provider routes; `personality` controls only the friendly interaction-style layer. Native Codex app-server routes keep Codex-owned base/model instructions instead of this OpenClaw GPT-5 overlay, and OpenClaw disables Codex's built-in personality for native threads. ```json5 { @@ -653,7 +653,7 @@ Periodic heartbeat runs. identifierPolicy: "strict", // strict | off | custom identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom qualityGuard: { enabled: true, maxRetries: 1 }, - midTurnPrecheck: { enabled: false }, // optional Pi tool-loop pressure check + midTurnPrecheck: { enabled: false }, // optional tool-loop pressure check postCompactionSections: ["Session Startup", "Red Lines"], // opt in to AGENTS.md section reinjection model: "openrouter/anthropic/claude-sonnet-4-6", // optional compaction-only model override truncateAfterCompaction: true, // rotate to a smaller successor JSONL after compaction @@ -675,11 +675,11 @@ Periodic heartbeat runs. - `mode`: `default` or `safeguard` (chunked summarization for long histories). See [Compaction](/concepts/compaction). - `provider`: id of a registered compaction provider plugin. When set, the provider's `summarize()` is called instead of built-in LLM summarization. Falls back to built-in on failure. Setting a provider forces `mode: "safeguard"`. See [Compaction](/concepts/compaction). - `timeoutSeconds`: maximum seconds allowed for a single compaction operation before OpenClaw aborts it. Default: `900`. -- `keepRecentTokens`: Pi cut-point budget for keeping the most recent transcript tail verbatim. Manual `/compact` honors this when explicitly set; otherwise manual compaction is a hard checkpoint. +- `keepRecentTokens`: agent cut-point budget for keeping the most recent transcript tail verbatim. Manual `/compact` honors this when explicitly set; otherwise manual compaction is a hard checkpoint. - `identifierPolicy`: `strict` (default), `off`, or `custom`. `strict` prepends built-in opaque identifier retention guidance during compaction summarization. - `identifierInstructions`: optional custom identifier-preservation text used when `identifierPolicy=custom`. - `qualityGuard`: retry-on-malformed-output checks for safeguard summaries. Enabled by default in safeguard mode; set `enabled: false` to skip the audit. -- `midTurnPrecheck`: optional Pi tool-loop pressure check. When `enabled: true`, OpenClaw checks context pressure after tool results are appended and before the next model call. If the context no longer fits, it aborts the current attempt before submitting the prompt and reuses the existing precheck recovery path to truncate tool results or compact and retry. Works with both `default` and `safeguard` compaction modes. Default: disabled. +- `midTurnPrecheck`: optional tool-loop pressure check. When `enabled: true`, OpenClaw checks context pressure after tool results are appended and before the next model call. If the context no longer fits, it aborts the current attempt before submitting the prompt and reuses the existing precheck recovery path to truncate tool results or compact and retry. Works with both `default` and `safeguard` compaction modes. Default: disabled. - `postCompactionSections`: optional AGENTS.md H2/H3 section names to re-inject after compaction. Reinjection is disabled when unset or set to `[]`. Explicitly setting `["Session Startup", "Red Lines"]` enables that pair and preserves the legacy `Every Session`/`Safety` fallback. Enable this only when the extra context is worth the risk of duplicating project guidance already captured in the compaction summary. - `model`: optional `provider/model-id` override for compaction summarization only. Use this when the main session should keep one model but compaction summaries should run on another; when unset, compaction uses the session's primary model. - `maxActiveTranscriptBytes`: optional byte threshold (`number` or strings like `"20mb"`) that triggers normal local compaction before a run when the active JSONL grows past the threshold. Requires `truncateAfterCompaction` so successful compaction can rotate to a smaller successor transcript. Disabled when unset or `0`. @@ -688,7 +688,7 @@ Periodic heartbeat runs. ### `agents.defaults.runRetries` -Outer run loop retry iteration boundaries for the embedded Pi runner to prevent infinite execution loops during failure recovery. Note that this setting currently only applies to the embedded agent runtime, not ACP or CLI runtimes. +Outer run loop retry iteration boundaries for the embedded agent runtime to prevent infinite execution loops during failure recovery. Note that this setting currently only applies to the embedded agent runtime, not ACP or CLI runtimes. ```json5 { diff --git a/docs/gateway/config-tools.md b/docs/gateway/config-tools.md index 4575867c7e4..88a57531c23 100644 --- a/docs/gateway/config-tools.md +++ b/docs/gateway/config-tools.md @@ -413,7 +413,7 @@ Experimental built-in tool flags. Default off unless a strict-agentic GPT-5 auto ``` - `planTool`: enables the structured `update_plan` tool for non-trivial multi-step work tracking. -- Default: `false` unless `agents.defaults.embeddedPi.executionContract` (or a per-agent override) is set to `"strict-agentic"` for an OpenAI or OpenAI Codex GPT-5-family run. Set `true` to force the tool on outside that scope, or `false` to keep it off even for strict-agentic GPT-5 runs. +- Default: `false` unless `agents.defaults.embeddedAgent.executionContract` (or a per-agent override) is set to `"strict-agentic"` for an OpenAI or OpenAI Codex GPT-5-family run. Set `true` to force the tool on outside that scope, or `false` to keep it off even for strict-agentic GPT-5 runs. - When enabled, the system prompt also adds usage guidance so the model only uses it for substantial work and keeps at most one step `in_progress`. ### `agents.defaults.subagents` @@ -479,7 +479,7 @@ Configuring a custom/local provider `baseUrl` is also the narrow network trust d - Use `authHeader: true` + `headers` for custom auth needs. - - Override agent config root with `OPENCLAW_AGENT_DIR` (or `PI_CODING_AGENT_DIR`, a legacy environment variable alias). + - Override agent config root with `OPENCLAW_AGENT_DIR`. - Merge precedence for matching provider IDs: - Non-empty agent `models.json` `baseUrl` values win. - Non-empty agent `apiKey` values win only when that provider is not SecretRef-managed in current config/auth-profile context. diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 627b5d3ef7d..7a17a42b841 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -89,7 +89,7 @@ The `models` root also owns global model-catalog behavior. ## MCP OpenClaw-managed MCP server definitions live under `mcp.servers` and are -consumed by embedded Pi and other runtime adapters. The `openclaw mcp list`, +consumed by embedded OpenClaw and other runtime adapters. The `openclaw mcp list`, `show`, `set`, and `unset` commands manage this block without connecting to the target server during config edits. @@ -243,7 +243,7 @@ The bundled `codex` plugin owns native Codex app-server harness settings under surface and [Codex harness](/plugins/codex-harness) for the runtime model. `codexPlugins` applies only to sessions that select the native Codex harness. -It does not enable Codex plugins for Pi, normal OpenAI provider runs, ACP +It does not enable Codex plugins for OpenClaw provider runs, ACP conversation bindings, or any non-Codex harness. ```json5 @@ -319,7 +319,7 @@ restart after changing native plugin config. - `memory.citations` - `memory.qmd.*` - `plugins.entries.memory-core.config.dreaming` -- Enabled Claude bundle plugins can also contribute embedded Pi defaults from `settings.json`; OpenClaw applies those as sanitized agent settings, not as raw OpenClaw config patches. +- Enabled Claude bundle plugins can also contribute embedded OpenClaw defaults from `settings.json`; OpenClaw applies those as sanitized agent settings, not as raw OpenClaw config patches. - `plugins.slots.memory`: pick the active memory plugin id, or `"none"` to disable memory plugins. - `plugins.slots.contextEngine`: pick the active context engine plugin id; defaults to `"legacy"` unless you install and select another engine. diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index 50c4bd38b10..329f0c0d1b4 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -293,7 +293,7 @@ That stages grounded durable candidates into the short-term dreaming store while - If you've added `models.providers.opencode`, `opencode-zen`, or `opencode-go` manually, it overrides the built-in OpenCode catalog from `@earendil-works/pi-ai`. That can force models onto the wrong API or zero out costs. Doctor warns so you can remove the override and restore per-model API routing + costs. + If you've added `models.providers.opencode`, `opencode-zen`, or `opencode-go` manually, it overrides the built-in OpenCode catalog from `openclaw/plugin-sdk/llm`. That can force models onto the wrong API or zero out costs. Doctor warns so you can remove the override and restore per-model API routing + costs. If your browser config still points at the removed Chrome extension path, doctor normalizes it to the current host-local Chrome MCP attach model: @@ -326,7 +326,7 @@ That stages grounded durable candidates into the short-term dreaming store while If you previously added legacy OpenAI transport settings under `models.providers.openai-codex`, they can shadow the built-in Codex OAuth provider path that newer releases use automatically. Doctor warns when it sees those old transport settings alongside Codex OAuth so you can remove or rewrite the stale transport override and get the built-in routing/fallback behavior back. Custom proxies and header-only overrides are still supported and do not trigger this warning. - Doctor checks for legacy `openai-codex/*` model refs. Native Codex harness routing uses canonical `openai/*` model refs; OpenAI agent turns go through the Codex app-server harness instead of the OpenClaw PI OpenAI path. + Doctor checks for legacy `openai-codex/*` model refs. Native Codex harness routing uses canonical `openai/*` model refs; OpenAI agent turns go through the Codex app-server harness instead of the OpenClaw OpenAI provider path. In `--fix` / `--repair` mode, doctor rewrites affected default-agent and per-agent refs, including primary models, fallbacks, heartbeat/subagent/compaction overrides, hooks, channel model overrides, and stale persisted session route state: diff --git a/docs/help/debugging.md b/docs/help/debugging.md index c3e110a07d1..1929b08c858 100644 --- a/docs/help/debugging.md +++ b/docs/help/debugging.md @@ -278,27 +278,24 @@ Default file: `~/.openclaw/logs/raw-stream.jsonl` -## Raw chunk logging (pi-mono) +## Raw OpenAI-compatible chunk logging To capture **raw OpenAI-compat chunks** before they are parsed into blocks, -pi-mono exposes a separate logger: +enable the transport logger: ```bash -PI_RAW_STREAM=1 +OPENCLAW_RAW_STREAM=1 ``` Optional path: ```bash -PI_RAW_STREAM_PATH=~/.pi-mono/logs/raw-openai-completions.jsonl +OPENCLAW_RAW_STREAM_PATH=~/.openclaw/logs/raw-openai-completions.jsonl ``` Default file: -`~/.pi-mono/logs/raw-openai-completions.jsonl` - -> Note: this is only emitted by processes using pi-mono's -> `openai-completions` provider. +`~/.openclaw/logs/raw-openai-completions.jsonl` ## Safety notes diff --git a/docs/help/gpt55-codex-agentic-parity-maintainers.md b/docs/help/gpt55-codex-agentic-parity-maintainers.md index 2cf69baadc9..b5672af9b50 100644 --- a/docs/help/gpt55-codex-agentic-parity-maintainers.md +++ b/docs/help/gpt55-codex-agentic-parity-maintainers.md @@ -94,7 +94,7 @@ PR D is the proof layer. It should not be the reason runtime-correctness PRs are - GPT-5 runs act or fail closed instead of stopping at commentary - `update_plan` no longer looks like progress by itself -- behavior stays GPT-5-first and embedded-Pi scoped +- behavior stays GPT-5-first and embedded-OpenClaw scoped ### PR B diff --git a/docs/help/gpt55-codex-agentic-parity.md b/docs/help/gpt55-codex-agentic-parity.md index dd9ed85dfa0..ac4cbf3af62 100644 --- a/docs/help/gpt55-codex-agentic-parity.md +++ b/docs/help/gpt55-codex-agentic-parity.md @@ -21,7 +21,7 @@ This parity program fixes those gaps in four reviewable slices. ### PR A: strict-agentic execution -This slice adds an opt-in `strict-agentic` execution contract for embedded Pi GPT-5 runs. +This slice adds an opt-in `strict-agentic` execution contract for embedded OpenClaw GPT-5 runs. When enabled, OpenClaw stops accepting plan-only turns as "good enough" completion. If the model only says what it intends to do and does not actually use tools or make progress, OpenClaw retries with an act-now steer and then fails closed with an explicit blocked state instead of silently ending the task. @@ -104,7 +104,7 @@ to: ```mermaid flowchart TD - A["User request"] --> B["Embedded Pi runtime"] + A["User request"] --> B["Embedded OpenClaw runtime"] B --> C["Strict-agentic execution contract"] B --> D["Provider-owned tool compatibility"] B --> E["Runtime truthfulness"] diff --git a/docs/help/testing-live.md b/docs/help/testing-live.md index 7c5fbc31b2f..ea71a1c611c 100644 --- a/docs/help/testing-live.md +++ b/docs/help/testing-live.md @@ -296,7 +296,7 @@ Docker notes: - Optional MCP/tool probe: `OPENCLAW_LIVE_CODEX_HARNESS_MCP_PROBE=1` - Optional Guardian probe: `OPENCLAW_LIVE_CODEX_HARNESS_GUARDIAN_PROBE=1` - The smoke forces provider/model `agentRuntime.id: "codex"` so a broken Codex - harness cannot pass by silently falling back to PI. + harness cannot pass by silently falling back to OpenClaw. - Auth: Codex app-server auth from the local Codex subscription login. Docker smokes can also provide `OPENAI_API_KEY` for non-Codex probes when applicable, plus optional copied `~/.codex/auth.json` and `~/.codex/config.toml`. @@ -329,7 +329,7 @@ Docker notes: `OPENCLAW_LIVE_CODEX_HARNESS_MCP_PROBE=0` or `OPENCLAW_LIVE_CODEX_HARNESS_GUARDIAN_PROBE=0` when you need a narrower debug run. -- Docker uses the same explicit Codex runtime config, so legacy aliases or PI +- Docker uses the same explicit Codex runtime config, so legacy aliases or OpenClaw fallback cannot hide a Codex harness regression. ### Recommended live recipes diff --git a/docs/help/testing.md b/docs/help/testing.md index cdaf9b7ebdd..e2ef872b414 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -535,9 +535,9 @@ Native dependency policy: - Add focused helper regressions for pure routing and normalization boundaries. - Keep the embedded runner integration suites healthy: - `src/agents/pi-embedded-runner/compact.hooks.test.ts`, - `src/agents/pi-embedded-runner/run.overflow-compaction.test.ts`, and - `src/agents/pi-embedded-runner/run.overflow-compaction.loop.test.ts`. + `src/agents/embedded-agent-runner/compact.hooks.test.ts`, + `src/agents/embedded-agent-runner/run.overflow-compaction.test.ts`, and + `src/agents/embedded-agent-runner/run.overflow-compaction.loop.test.ts`. - Those suites verify that scoped ids and compaction behavior still flow through the real `run.ts` / `compact.ts` paths; helper-only tests are not a sufficient substitute for those integration paths. @@ -749,7 +749,7 @@ These Docker runners split into two buckets: - `Package Acceptance` is the GitHub-native package gate for "does this installable tarball work as a product?" It resolves one candidate package from `source=npm`, `source=ref`, `source=url`, or `source=artifact`, uploads it as `package-under-test`, then runs the reusable Docker E2E lanes against that exact tarball instead of repacking the selected ref. Profiles are ordered by breadth: `smoke`, `package`, `product`, and `full`. See [Testing updates and plugins](/help/testing-updates-plugins) for the package/update/plugin contract, published-upgrade survivor matrix, release defaults, and failure triage. - Build and release checks run `scripts/check-cli-bootstrap-imports.mjs` after tsdown. The guard walks the static built graph from `dist/entry.js` and `dist/cli/run-main.js` and fails if pre-dispatch startup imports package dependencies such as Commander, prompt UI, undici, or logging before command dispatch; it also keeps the bundled gateway run chunk under budget and rejects static imports of known cold gateway paths. Packaged CLI smoke also covers root help, onboard help, doctor help, status, config schema, and a model-list command. - Package Acceptance legacy compatibility is capped at `2026.4.25` (`2026.4.25-beta.*` included). Through that cutoff, the harness tolerates only shipped-package metadata gaps: omitted private QA inventory entries, missing `gateway install --wrapper`, missing patch files in the tarball-derived git fixture, missing persisted `update.channel`, legacy plugin install-record locations, missing marketplace install-record persistence, and config metadata migration during `plugins update`. For packages after `2026.4.25`, those paths are strict failures. -- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:release-user-journey`, `test:docker:release-typed-onboarding`, `test:docker:release-media-memory`, `test:docker:release-upgrade-user-journey`, `test:docker:release-plugin-marketplace`, `test:docker:skill-install`, `test:docker:update-channel-switch`, `test:docker:upgrade-survivor`, `test:docker:published-upgrade-survivor`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, `test:docker:plugin-lifecycle-matrix`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths. +- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:release-user-journey`, `test:docker:release-typed-onboarding`, `test:docker:release-media-memory`, `test:docker:release-upgrade-user-journey`, `test:docker:release-plugin-marketplace`, `test:docker:skill-install`, `test:docker:update-channel-switch`, `test:docker:upgrade-survivor`, `test:docker:published-upgrade-survivor`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:agent-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, `test:docker:plugin-lifecycle-matrix`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths. - Docker/Bash E2E lanes that install the packed OpenClaw tarball through `scripts/lib/openclaw-e2e-instance.sh` cap `npm install` at `OPENCLAW_E2E_NPM_INSTALL_TIMEOUT` (default `600s`; set `0` to disable the wrapper for debugging). The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store: @@ -782,7 +782,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or - Browser CDP snapshot smoke: `pnpm test:docker:browser-cdp-snapshot` (script: `scripts/e2e/browser-cdp-snapshot-docker.sh`) builds the source E2E image plus a Chromium layer, starts Chromium with raw CDP, runs `browser doctor --deep`, and verifies CDP role snapshots cover link URLs, cursor-promoted clickables, iframe refs, and frame metadata. - OpenAI Responses web_search minimal reasoning regression: `pnpm test:docker:openai-web-search-minimal` (script: `scripts/e2e/openai-web-search-minimal-docker.sh`) runs a mocked OpenAI server through Gateway, verifies `web_search` raises `reasoning.effort` from `minimal` to `low`, then forces the provider schema reject and checks the raw detail appears in Gateway logs. - MCP channel bridge (seeded Gateway + stdio bridge + raw Claude notification-frame smoke): `pnpm test:docker:mcp-channels` (script: `scripts/e2e/mcp-channels-docker.sh`) -- Pi bundle MCP tools (real stdio MCP server + embedded Pi profile allow/deny smoke): `pnpm test:docker:pi-bundle-mcp-tools` (script: `scripts/e2e/pi-bundle-mcp-tools-docker.sh`) +- OpenClaw bundle MCP tools (real stdio MCP server + embedded OpenClaw profile allow/deny smoke): `pnpm test:docker:agent-bundle-mcp-tools` (script: `scripts/e2e/agent-bundle-mcp-tools-docker.sh`) - Cron/subagent MCP cleanup (real Gateway + stdio MCP child teardown after isolated cron and one-shot subagent runs): `pnpm test:docker:cron-mcp-cleanup` (script: `scripts/e2e/cron-mcp-cleanup-docker.sh`) - Plugins (install/update smoke for local path, `file:`, npm registry with hoisted dependencies, malformed npm package metadata, git moving refs, ClawHub kitchen-sink, marketplace updates, and Claude-bundle enable/inspect): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`) Set `OPENCLAW_PLUGINS_E2E_CLAWHUB=0` to skip the ClawHub block, or override the default kitchen-sink package/runtime pair with `OPENCLAW_PLUGINS_E2E_CLAWHUB_SPEC` and `OPENCLAW_PLUGINS_E2E_CLAWHUB_ID`. Without `OPENCLAW_CLAWHUB_URL`/`CLAWHUB_URL`, the test uses a hermetic local ClawHub fixture server. @@ -834,9 +834,9 @@ live event queue behavior, outbound send routing, and Claude-style channel + permission notifications over the real stdio MCP bridge. The notification check inspects the raw stdio MCP frames directly so the smoke validates what the bridge actually emits, not just what a specific client SDK happens to surface. -`test:docker:pi-bundle-mcp-tools` is deterministic and does not need a live +`test:docker:agent-bundle-mcp-tools` is deterministic and does not need a live model key. It builds the repo Docker image, starts a real stdio MCP probe server -inside the container, materializes that server through the embedded Pi bundle +inside the container, materializes that server through the embedded OpenClaw bundle MCP runtime, executes the tool, then verifies `coding` and `messaging` keep `bundle-mcp` tools while `minimal` and `tools.deny: ["bundle-mcp"]` filter them. `test:docker:cron-mcp-cleanup` is deterministic and does not need a live model diff --git a/docs/index.md b/docs/index.md index 0e6f2d429cf..7b962d53c7c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -43,7 +43,7 @@ title: "OpenClaw" ## What is OpenClaw? -OpenClaw is a **self-hosted gateway** that connects your favorite chat apps and channel surfaces — built-in channels plus bundled or external channel plugins such as Discord, Google Chat, iMessage, Matrix, Microsoft Teams, Signal, Slack, Telegram, WhatsApp, Zalo, and more — to AI coding agents like Pi. You run a single Gateway process on your own machine (or a server), and it becomes the bridge between your messaging apps and an always-available AI assistant. +OpenClaw is a **self-hosted gateway** that connects your favorite chat apps and channel surfaces — built-in channels plus bundled or external channel plugins such as Discord, Google Chat, iMessage, Matrix, Microsoft Teams, Signal, Slack, Telegram, WhatsApp, Zalo, and more — to AI coding agents. You run a single Gateway process on your own machine (or a server), and it becomes the bridge between your messaging apps and an always-available AI assistant. **Who is it for?** Developers and power users who want a personal AI assistant they can message from anywhere — without giving up control of their data or relying on a hosted service. @@ -61,7 +61,7 @@ OpenClaw is a **self-hosted gateway** that connects your favorite chat apps and ```mermaid flowchart LR A["Chat apps + plugins"] --> B["Gateway"] - B --> C["Pi agent"] + B --> C["OpenClaw agent"] B --> D["CLI"] B --> E["Web Control UI"] B --> F["macOS app"] @@ -135,7 +135,7 @@ Open the browser Control UI after the Gateway starts. Config lives at `~/.openclaw/openclaw.json`. -- If you **do nothing**, OpenClaw uses the bundled Pi binary in RPC mode with per-sender sessions. +- If you **do nothing**, OpenClaw uses the bundled OpenClaw agent runtime with per-sender sessions. - If you want to lock it down, start with `channels.whatsapp.allowFrom` and (for groups) mention rules. Example: diff --git a/docs/nodes/images.md b/docs/nodes/images.md index 94a887cbd1f..f1c340a2094 100644 --- a/docs/nodes/images.md +++ b/docs/nodes/images.md @@ -37,7 +37,7 @@ The WhatsApp channel runs via **Baileys Web**. This document captures the curren - When media is present, the web sender resolves local paths or URLs using the same pipeline as `openclaw message send`. - Multiple media entries are sent sequentially if provided. -## Inbound media to commands (Pi) +## Inbound Media To Commands - When inbound web messages include media, OpenClaw downloads to a temp file and exposes templating variables: - `{{MediaUrl}}` pseudo-URL for the inbound media. diff --git a/docs/pi-dev.md b/docs/openclaw-agent-runtime.md similarity index 57% rename from docs/pi-dev.md rename to docs/openclaw-agent-runtime.md index d9cf6028d67..c51f787d78c 100644 --- a/docs/pi-dev.md +++ b/docs/openclaw-agent-runtime.md @@ -1,47 +1,47 @@ --- -summary: "Developer workflow for Pi integration: build, test, and live validation" -title: "Pi development workflow" +summary: "Developer workflow for OpenClaw agent runtime: build, test, and live validation" +title: "OpenClaw agent runtime workflow" read_when: - - Working on Pi integration code or tests - - Running Pi-specific lint, typecheck, and live test flows + - Working on OpenClaw agent runtime code or tests + - Running agent-runtime lint, typecheck, and live test flows --- -A sane workflow for working on the Pi integration in OpenClaw. +A sane workflow for working on the OpenClaw agent runtime in OpenClaw. ## Type checking and linting - Default local gate: `pnpm check` - Build gate: `pnpm build` when the change can affect build output, packaging, or lazy-loading/module boundaries -- Full landing gate for Pi-heavy changes: `pnpm check && pnpm test` +- Full landing gate for agent-runtime changes: `pnpm check && pnpm test` -## Running Pi tests +## Running Agent Runtime Tests -Run the Pi-focused test set directly with Vitest: +Run the agent-runtime test set directly with Vitest: ```bash pnpm test \ - "src/agents/pi-*.test.ts" \ - "src/agents/pi-embedded-*.test.ts" \ - "src/agents/pi-tools*.test.ts" \ - "src/agents/pi-settings.test.ts" \ - "src/agents/pi-tool-definition-adapter*.test.ts" \ - "src/agents/pi-hooks/**/*.test.ts" + "src/agents/agent-*.test.ts" \ + "src/agents/embedded-agent-*.test.ts" \ + "src/agents/agent-tools*.test.ts" \ + "src/agents/agent-settings.test.ts" \ + "src/agents/agent-tool-definition-adapter*.test.ts" \ + "src/agents/agent-hooks/**/*.test.ts" ``` To include the live provider exercise: ```bash -OPENCLAW_LIVE_TEST=1 pnpm test src/agents/pi-embedded-runner-extraparams.live.test.ts +OPENCLAW_LIVE_TEST=1 pnpm test src/agents/embedded-agent-runner-extraparams.live.test.ts ``` -This covers the main Pi unit suites: +This covers the main agent runtime unit suites: -- `src/agents/pi-*.test.ts` -- `src/agents/pi-embedded-*.test.ts` -- `src/agents/pi-tools*.test.ts` -- `src/agents/pi-settings.test.ts` -- `src/agents/pi-tool-definition-adapter.test.ts` -- `src/agents/pi-hooks/*.test.ts` +- `src/agents/agent-*.test.ts` +- `src/agents/embedded-agent-*.test.ts` +- `src/agents/agent-tools*.test.ts` +- `src/agents/agent-settings.test.ts` +- `src/agents/agent-tool-definition-adapter.test.ts` +- `src/agents/agent-hooks/*.test.ts` ## Manual testing @@ -79,4 +79,4 @@ If you only want to reset sessions, delete `agents//sessions/` for that ## Related -- [Pi integration architecture](/pi) +- [OpenClaw agent runtime architecture](/agent-runtime-architecture) diff --git a/docs/pi.md b/docs/pi.md index 12ca5ac5646..cb1a39a5359 100644 --- a/docs/pi.md +++ b/docs/pi.md @@ -1,573 +1,7 @@ --- -summary: "Architecture of OpenClaw's embedded Pi agent integration and session lifecycle" -title: "Pi integration architecture" -read_when: - - Understanding Pi SDK integration design in OpenClaw - - Modifying agent session lifecycle, tooling, or provider wiring for Pi +summary: "Redirect to /agent-runtime-architecture" +title: "Agent runtime architecture (legacy)" +redirect: /agent-runtime-architecture --- -OpenClaw integrates with [pi-coding-agent](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) and its sibling packages (`pi-ai`, `pi-agent-core`, `pi-tui`) to power its AI agent capabilities. - -## Overview - -OpenClaw uses the pi SDK to embed an AI coding agent into its messaging gateway architecture. Instead of spawning pi as a subprocess or using RPC mode, OpenClaw directly imports and instantiates pi's `AgentSession` via `createAgentSession()`. This embedded approach provides: - -- Full control over session lifecycle and event handling -- Custom tool injection (messaging, sandbox, channel-specific actions) -- System prompt customization per channel/context -- Session persistence with branching/compaction support -- Multi-account auth profile rotation with failover -- Provider-agnostic model switching - -## Package dependencies - -```json -{ - "@earendil-works/pi-agent-core": "0.75.1", - "@earendil-works/pi-ai": "0.75.1", - "@earendil-works/pi-coding-agent": "0.75.1", - "@earendil-works/pi-tui": "0.75.1" -} -``` - -| Package | Purpose | -| ----------------- | ------------------------------------------------------------------------------------------------------ | -| `pi-ai` | Core LLM abstractions: `Model`, `streamSimple`, message types, provider APIs | -| `pi-agent-core` | Agent loop, tool execution, `AgentMessage` types | -| `pi-coding-agent` | High-level SDK: `createAgentSession`, `SessionManager`, `AuthStorage`, `ModelRegistry`, built-in tools | -| `pi-tui` | Terminal UI components (used in OpenClaw's local TUI mode) | - -## File structure - -``` -src/agents/ -├── pi-embedded-runner.ts # Re-exports from pi-embedded-runner/ -├── pi-embedded-runner/ -│ ├── run.ts # Main entry: runEmbeddedPiAgent() -│ ├── run/ -│ │ ├── attempt.ts # Single attempt logic with session setup -│ │ ├── params.ts # RunEmbeddedPiAgentParams type -│ │ ├── payloads.ts # Build response payloads from run results -│ │ ├── images.ts # Vision model image injection -│ │ └── types.ts # EmbeddedRunAttemptResult -│ ├── abort.ts # Abort error detection -│ ├── cache-ttl.ts # Cache TTL tracking for context pruning -│ ├── compact.ts # Manual/auto compaction logic -│ ├── extensions.ts # Load pi extensions for embedded runs -│ ├── extra-params.ts # Provider-specific stream params -│ ├── google.ts # Google/Gemini turn ordering fixes -│ ├── history.ts # History limiting (DM vs group) -│ ├── lanes.ts # Session/global command lanes -│ ├── logger.ts # Subsystem logger -│ ├── model.ts # Model resolution via ModelRegistry -│ ├── runs.ts # Active run tracking, abort, queue -│ ├── sandbox-info.ts # Sandbox info for system prompt -│ ├── session-manager-cache.ts # SessionManager instance caching -│ ├── session-manager-init.ts # Session file initialization -│ ├── system-prompt.ts # System prompt builder -│ ├── tool-split.ts # Split tools into builtIn vs custom -│ ├── types.ts # EmbeddedPiAgentMeta, EmbeddedPiRunResult -│ └── utils.ts # ThinkLevel mapping, error description -├── pi-embedded-subscribe.ts # Session event subscription/dispatch -├── pi-embedded-subscribe.types.ts # SubscribeEmbeddedPiSessionParams -├── pi-embedded-subscribe.handlers.ts # Event handler factory -├── pi-embedded-subscribe.handlers.lifecycle.ts -├── pi-embedded-subscribe.handlers.types.ts -├── pi-embedded-block-chunker.ts # Streaming block reply chunking -├── pi-embedded-messaging.ts # Messaging tool sent tracking -├── pi-embedded-helpers.ts # Error classification, turn validation -├── pi-embedded-helpers/ # Helper modules -├── pi-embedded-utils.ts # Formatting utilities -├── pi-tools.ts # createOpenClawCodingTools() -├── pi-tools.abort.ts # AbortSignal wrapping for tools -├── pi-tools.policy.ts # Tool allowlist/denylist policy -├── pi-tools.read.ts # Read tool customizations -├── pi-tools.schema.ts # Tool schema normalization -├── pi-tools.types.ts # AnyAgentTool type alias -├── pi-tool-definition-adapter.ts # AgentTool -> ToolDefinition adapter -├── pi-settings.ts # Settings overrides -├── pi-hooks/ # Custom pi hooks -│ ├── compaction-safeguard.ts # Safeguard extension -│ ├── compaction-safeguard-runtime.ts -│ ├── context-pruning.ts # Cache-TTL context pruning extension -│ └── context-pruning/ -├── model-auth.ts # Auth profile resolution -├── auth-profiles.ts # Profile store, cooldown, failover -├── model-selection.ts # Default model resolution -├── models-config.ts # models.json generation -├── model-catalog.ts # Model catalog cache -├── context-window-guard.ts # Context window validation -├── failover-error.ts # FailoverError class -├── defaults.ts # DEFAULT_PROVIDER, DEFAULT_MODEL -├── system-prompt.ts # buildAgentSystemPrompt() -├── system-prompt-params.ts # System prompt parameter resolution -├── system-prompt-report.ts # Debug report generation -├── tool-summaries.ts # Tool description summaries -├── tool-policy.ts # Tool policy resolution -├── transcript-policy.ts # Transcript validation policy -├── skills.ts # Skill snapshot/prompt building -├── skills/ # Skill subsystem -├── sandbox.ts # Sandbox context resolution -├── sandbox/ # Sandbox subsystem -├── channel-tools.ts # Channel-specific tool injection -├── openclaw-tools.ts # OpenClaw-specific tools -├── bash-tools.ts # exec/process tools -├── apply-patch.ts # apply_patch tool (OpenAI) -├── tools/ # Individual tool implementations -│ ├── browser-tool.ts -│ ├── canvas-tool.ts -│ ├── cron-tool.ts -│ ├── gateway-tool.ts -│ ├── image-tool.ts -│ ├── message-tool.ts -│ ├── nodes-tool.ts -│ ├── session*.ts -│ ├── web-*.ts -│ └── ... -└── ... -``` - -Channel-specific message action runtimes now live in the plugin-owned extension -directories instead of under `src/agents/tools`, for example: - -- the Discord plugin action runtime files -- the Slack plugin action runtime file -- the Telegram plugin action runtime file -- the WhatsApp plugin action runtime file - -## Core integration flow - -### 1. Running an Embedded Agent - -The main entry point is `runEmbeddedPiAgent()` in `pi-embedded-runner/run.ts`: - -```typescript -import { runEmbeddedPiAgent } from "./agents/pi-embedded-runner.js"; - -const result = await runEmbeddedPiAgent({ - sessionId: "user-123", - sessionKey: "main:whatsapp:+1234567890", - sessionFile: "/path/to/session.jsonl", - workspaceDir: "/path/to/workspace", - config: openclawConfig, - prompt: "Hello, how are you?", - provider: "anthropic", - model: "claude-sonnet-4-6", - timeoutMs: 120_000, - runId: "run-abc", - onBlockReply: async (payload) => { - await sendToChannel(payload.text, payload.mediaUrls); - }, -}); -``` - -### 2. Session Creation - -Inside `runEmbeddedAttempt()` (called by `runEmbeddedPiAgent()`), the pi SDK is used: - -```typescript -import { - createAgentSession, - DefaultResourceLoader, - SessionManager, - SettingsManager, -} from "@earendil-works/pi-coding-agent"; - -const resourceLoader = new DefaultResourceLoader({ - cwd: resolvedWorkspace, - agentDir, - settingsManager, - additionalExtensionPaths, -}); -await resourceLoader.reload(); - -const { session } = await createAgentSession({ - cwd: resolvedWorkspace, - agentDir, - authStorage: params.authStorage, - modelRegistry: params.modelRegistry, - model: params.model, - thinkingLevel: mapThinkingLevel(params.thinkLevel), - tools: builtInTools, - customTools: allCustomTools, - sessionManager, - settingsManager, - resourceLoader, -}); - -applySystemPromptOverrideToSession(session, systemPromptOverride); -``` - -### 3. Event Subscription - -`subscribeEmbeddedPiSession()` subscribes to pi's `AgentSession` events: - -```typescript -const subscription = subscribeEmbeddedPiSession({ - session: activeSession, - runId: params.runId, - verboseLevel: params.verboseLevel, - reasoningMode: params.reasoningLevel, - toolResultFormat: params.toolResultFormat, - onToolResult: params.onToolResult, - onReasoningStream: params.onReasoningStream, - onBlockReply: params.onBlockReply, - onPartialReply: params.onPartialReply, - onAgentEvent: params.onAgentEvent, -}); -``` - -Events handled include: - -- `message_start` / `message_end` / `message_update` (streaming text/thinking) -- `tool_execution_start` / `tool_execution_update` / `tool_execution_end` -- `turn_start` / `turn_end` -- `agent_start` / `agent_end` -- `compaction_start` / `compaction_end` - -### 4. Prompting - -After setup, the session is prompted: - -```typescript -await session.prompt(effectivePrompt, { images: imageResult.images }); -``` - -The SDK handles the full agent loop: sending to LLM, executing tool calls, streaming responses. - -Image injection is prompt-local: OpenClaw loads image refs from the current prompt and -passes them via `images` for that turn only. It does not re-scan older history turns -to re-inject image payloads. - -## Tool architecture - -### Tool pipeline - -1. **Base Tools**: pi's `codingTools` (read, bash, edit, write) -2. **Custom Replacements**: OpenClaw replaces bash with `exec`/`process`, customizes read/edit/write for sandbox -3. **OpenClaw Tools**: messaging, browser, canvas, sessions, cron, gateway, etc. -4. **Channel Tools**: Discord/Telegram/Slack/WhatsApp-specific action tools -5. **Policy Filtering**: Tools filtered by profile, provider, agent, group, sandbox policies -6. **Schema Normalization**: Schemas cleaned for Gemini/OpenAI quirks -7. **AbortSignal Wrapping**: Tools wrapped to respect abort signals - -### Tool definition adapter - -pi-agent-core's `AgentTool` has a different `execute` signature than pi-coding-agent's `ToolDefinition`. The adapter in `pi-tool-definition-adapter.ts` bridges this: - -```typescript -export function toToolDefinitions(tools: AnyAgentTool[]): ToolDefinition[] { - return tools.map((tool) => ({ - name: tool.name, - label: tool.label ?? name, - description: tool.description ?? "", - parameters: tool.parameters, - execute: async (toolCallId, params, onUpdate, _ctx, signal) => { - // pi-coding-agent signature differs from pi-agent-core - return await tool.execute(toolCallId, params, signal, onUpdate); - }, - })); -} -``` - -### Tool split strategy - -`splitSdkTools()` passes all tools via `customTools`: - -```typescript -export function splitSdkTools(options: { tools: AnyAgentTool[]; sandboxEnabled: boolean }) { - return { - builtInTools: [], // Empty. We override everything - customTools: toToolDefinitions(options.tools), - }; -} -``` - -This ensures OpenClaw's policy filtering, sandbox integration, and extended toolset remain consistent across providers. - -## System prompt construction - -The system prompt is built in `buildAgentSystemPrompt()` (`system-prompt.ts`). It assembles a full prompt with sections including Tooling, Tool Call Style, Safety guardrails, OpenClaw Control, Skills, Docs, Workspace, Sandbox, Messaging, Assistant Output Directives, Voice, Silent Replies, Heartbeats, Runtime metadata, plus Memory and Reactions when enabled, and optional context files and extra system prompt content. Sections are trimmed for minimal prompt mode used by subagents. - -The prompt is applied after session creation via `applySystemPromptOverrideToSession()`: - -```typescript -const systemPromptOverride = createSystemPromptOverride(appendPrompt); -applySystemPromptOverrideToSession(session, systemPromptOverride); -``` - -## Session management - -### Session files - -Sessions are JSONL files with tree structure (id/parentId linking). Pi's `SessionManager` handles persistence: - -```typescript -const sessionManager = SessionManager.open(params.sessionFile); -``` - -OpenClaw wraps this with `guardSessionManager()` for tool result safety. - -### Session caching - -`session-manager-cache.ts` caches SessionManager instances to avoid repeated file parsing: - -```typescript -await prewarmSessionFile(params.sessionFile); -sessionManager = SessionManager.open(params.sessionFile); -trackSessionManagerAccess(params.sessionFile); -``` - -### History limiting - -`limitHistoryTurns()` trims conversation history based on channel type (DM vs group). - -### Compaction - -Auto-compaction triggers on context overflow. Common overflow signatures -include `request_too_large`, `context length exceeded`, `input exceeds the -maximum number of tokens`, `input token count exceeds the maximum number of -input tokens`, `input is too long for the model`, and `ollama error: context -length exceeded`. `compactEmbeddedPiSessionDirect()` handles manual -compaction: - -```typescript -const compactResult = await compactEmbeddedPiSessionDirect({ - sessionId, sessionFile, provider, model, ... -}); -``` - -## Authentication and model resolution - -### Auth profiles - -OpenClaw maintains an auth profile store with multiple API keys per provider: - -```typescript -const authStore = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); -const profileOrder = resolveAuthProfileOrder({ cfg, store: authStore, provider, preferredProfile }); -``` - -Profiles rotate on failures with cooldown tracking: - -```typescript -await markAuthProfileFailure({ store, profileId, reason, cfg, agentDir }); -const rotated = await advanceAuthProfile(); -``` - -### Model resolution - -```typescript -import { resolveModel } from "./pi-embedded-runner/model.js"; - -const { model, error, authStorage, modelRegistry } = resolveModel( - provider, - modelId, - agentDir, - config, -); - -// Uses pi's ModelRegistry and AuthStorage -authStorage.setRuntimeApiKey(model.provider, apiKeyInfo.apiKey); -``` - -### Failover - -`FailoverError` triggers model fallback when configured: - -```typescript -if (fallbackConfigured && isFailoverErrorMessage(errorText)) { - throw new FailoverError(errorText, { - reason: promptFailoverReason ?? "unknown", - provider, - model: modelId, - profileId, - status: resolveFailoverStatus(promptFailoverReason), - }); -} -``` - -## Pi extensions - -OpenClaw loads custom pi extensions for specialized behavior: - -### Compaction safeguard - -`src/agents/pi-hooks/compaction-safeguard.ts` adds guardrails to compaction, including adaptive token budgeting plus tool failure and file operation summaries: - -```typescript -if (resolveCompactionMode(params.cfg) === "safeguard") { - setCompactionSafeguardRuntime(params.sessionManager, { maxHistoryShare }); - paths.push(resolvePiExtensionPath("compaction-safeguard")); -} -``` - -### Context pruning - -`src/agents/pi-hooks/context-pruning.ts` implements cache-TTL based context pruning: - -```typescript -if (cfg?.agents?.defaults?.contextPruning?.mode === "cache-ttl") { - setContextPruningRuntime(params.sessionManager, { - settings, - contextWindowTokens, - isToolPrunable, - lastCacheTouchAt, - }); - paths.push(resolvePiExtensionPath("context-pruning")); -} -``` - -## Streaming and block replies - -### Block chunking - -`EmbeddedBlockChunker` manages streaming text into discrete reply blocks: - -```typescript -const blockChunker = blockChunking ? new EmbeddedBlockChunker(blockChunking) : null; -``` - -### Thinking/Final Tag Stripping - -Streaming output is processed to strip ``/`` blocks and extract `` content: - -```typescript -const stripBlockTags = (text: string, state: { thinking: boolean; final: boolean }) => { - // Strip ... content - // If enforceFinalTag, only return ... content -}; -``` - -### Reply directives - -Reply directives like `[[media:url]]`, `[[voice]]`, `[[reply:id]]` are parsed and extracted: - -```typescript -const { text: cleanedText, mediaUrls, audioAsVoice, replyToId } = consumeReplyDirectives(chunk); -``` - -## Error handling - -### Error classification - -`pi-embedded-helpers.ts` classifies errors for appropriate handling: - -```typescript -isContextOverflowError(errorText) // Context too large -isCompactionFailureError(errorText) // Compaction failed -isAuthAssistantError(lastAssistant) // Auth failure -isRateLimitAssistantError(...) // Rate limited -isFailoverAssistantError(...) // Should failover -classifyFailoverReason(errorText) // "auth" | "rate_limit" | "quota" | "timeout" | ... -``` - -### Thinking level fallback - -If a thinking level is unsupported, it falls back: - -```typescript -const fallbackThinking = pickFallbackThinkingLevel({ - message: errorText, - attempted: attemptedThinking, -}); -if (fallbackThinking) { - thinkLevel = fallbackThinking; - continue; -} -``` - -## Sandbox integration - -When sandbox mode is enabled, tools and paths are constrained: - -```typescript -const sandbox = await resolveSandboxContext({ - config: params.config, - sessionKey: sandboxSessionKey, - workspaceDir: resolvedWorkspace, -}); - -if (sandboxRoot) { - // Use sandboxed read/edit/write tools - // Exec runs in container - // Browser uses bridge URL -} -``` - -## Provider-Specific Handling - -### Anthropic - -- Refusal magic string scrubbing -- Turn validation for consecutive roles -- Strict upstream Pi tool parameter validation - -### Google/Gemini - -- Plugin-owned tool schema sanitization - -### OpenAI - -- `apply_patch` tool for Codex models -- Thinking level downgrade handling - -## TUI Integration - -OpenClaw also has a local TUI mode that uses pi-tui components directly: - -```typescript -// src/tui/tui.ts -import { ... } from "@earendil-works/pi-tui"; -``` - -This provides the interactive terminal experience similar to pi's native mode. - -## Key differences from Pi CLI - -| Aspect | Pi CLI | OpenClaw Embedded | -| --------------- | ----------------------- | ---------------------------------------------------------------------------------------------- | -| Invocation | `pi` command / RPC | SDK via `createAgentSession()` | -| Tools | Default coding tools | Custom OpenClaw tool suite | -| System prompt | AGENTS.md + prompts | Dynamic per-channel/context | -| Session storage | `~/.pi/agent/sessions/` | `~/.openclaw/agents//sessions/` (or `$OPENCLAW_STATE_DIR/agents//sessions/`) | -| Auth | Single credential | Multi-profile with rotation | -| Extensions | Loaded from disk | Programmatic + disk paths | -| Event handling | TUI rendering | Callback-based (onBlockReply, etc.) | - -## Future considerations - -Areas for potential rework: - -1. **Tool signature alignment**: Currently adapting between pi-agent-core and pi-coding-agent signatures -2. **Session manager wrapping**: `guardSessionManager` adds safety but increases complexity -3. **Extension loading**: Could use pi's `ResourceLoader` more directly -4. **Streaming handler complexity**: `subscribeEmbeddedPiSession` has grown large -5. **Provider quirks**: Many provider-specific codepaths that pi could potentially handle - -## Tests - -Pi integration coverage spans these suites: - -- `src/agents/pi-*.test.ts` -- `src/agents/pi-auth-json.test.ts` -- `src/agents/pi-embedded-*.test.ts` -- `src/agents/pi-embedded-helpers*.test.ts` -- `src/agents/pi-embedded-runner*.test.ts` -- `src/agents/pi-embedded-runner/**/*.test.ts` -- `src/agents/pi-embedded-subscribe*.test.ts` -- `src/agents/pi-tools*.test.ts` -- `src/agents/pi-tool-definition-adapter*.test.ts` -- `src/agents/pi-settings.test.ts` -- `src/agents/pi-hooks/**/*.test.ts` - -Live/opt-in: - -- `src/agents/pi-embedded-runner-extraparams.live.test.ts` (enable `OPENCLAW_LIVE_TEST=1`) - -For current run commands, see [Pi Development Workflow](/pi-dev). - -## Related - -- [Pi development workflow](/pi-dev) -- [Install overview](/install) +This page has moved to [Agent runtime architecture](/agent-runtime-architecture). diff --git a/docs/plan/codex-context-engine-harness.md b/docs/plan/codex-context-engine-harness.md index 009cc24ce4d..d2d90fbc34c 100644 --- a/docs/plan/codex-context-engine-harness.md +++ b/docs/plan/codex-context-engine-harness.md @@ -4,7 +4,7 @@ summary: "Specification for making the bundled Codex app-server harness honor Op read_when: - You are wiring context-engine lifecycle behavior into the Codex harness - You need lossless-claw or another context-engine plugin to work with codex/* embedded harness sessions - - You are comparing embedded PI and Codex app-server context behavior + - You are comparing embedded OpenClaw and Codex app-server context behavior --- ## Status @@ -14,10 +14,10 @@ Draft implementation specification. ## Goal Make the bundled Codex app-server harness honor the same OpenClaw context-engine -lifecycle contract that embedded PI turns already honor. +lifecycle contract that embedded OpenClaw turns already honor. -A session using `agents.defaults.embeddedHarness.runtime: "codex"` or a -`codex/*` model should still let the selected context-engine plugin, such as +A session using provider/model `agentRuntime.id: "codex"` or a `codex/*` model +should still let the selected context-engine plugin, such as `lossless-claw`, control context assembly, post-turn ingest, maintenance, and OpenClaw-level compaction policy as far as the Codex app-server boundary allows. @@ -36,7 +36,7 @@ OpenClaw-level compaction policy as far as the Codex app-server boundary allows. The embedded run loop resolves the configured context engine once per run before selecting a concrete low-level harness: -- `src/agents/pi-embedded-runner/run.ts` +- `src/agents/embedded-agent-runner/run.ts` - initializes context-engine plugins - calls `resolveContextEngine(params.config)` - passes `contextEngine` and `contextTokenBudget` into @@ -44,7 +44,7 @@ selecting a concrete low-level harness: `runEmbeddedAttemptWithBackend(...)` delegates to the selected agent harness: -- `src/agents/pi-embedded-runner/run/backend.ts` +- `src/agents/embedded-agent-runner/run/backend.ts` - `src/agents/harness/selection.ts` The Codex app-server harness is registered by the bundled Codex plugin: @@ -53,7 +53,7 @@ The Codex app-server harness is registered by the bundled Codex plugin: - `extensions/codex/harness.ts` The Codex harness implementation receives the same `EmbeddedRunAttemptParams` -as PI-backed attempts: +as built-in OpenClaw attempts: - `extensions/codex/src/app-server/run-attempt.ts` @@ -65,7 +65,7 @@ compactor. ## Current gap -Embedded PI attempts call the context-engine lifecycle directly: +Built-in OpenClaw attempts call the context-engine lifecycle directly: - bootstrap/maintenance before the attempt - assemble before the model call @@ -73,11 +73,11 @@ Embedded PI attempts call the context-engine lifecycle directly: - maintenance after a successful turn - context-engine compaction for engines that own compaction -Relevant PI code: +Relevant OpenClaw code: -- `src/agents/pi-embedded-runner/run/attempt.ts` -- `src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts` -- `src/agents/pi-embedded-runner/context-engine-maintenance.ts` +- `src/agents/embedded-agent-runner/run/attempt.ts` +- `src/agents/embedded-agent-runner/run/attempt.context-engine-helpers.ts` +- `src/agents/embedded-agent-runner/context-engine-maintenance.ts` Codex app-server attempts currently run generic agent-harness hooks and mirror the transcript, but do not call `params.contextEngine.bootstrap`, @@ -147,10 +147,10 @@ ordering to generated context text. Harness selection remains as-is: -- `runtime: "pi"` forces PI +- `runtime: "openclaw"` selects the built-in OpenClaw harness - `runtime: "codex"` selects the registered Codex harness - `runtime: "auto"` lets plugin harnesses claim supported providers -- unmatched `auto` runs use PI +- unmatched `auto` runs use the built-in OpenClaw harness This work changes what happens after the Codex harness is selected. @@ -158,14 +158,14 @@ This work changes what happens after the Codex harness is selected. ### 1. Export or relocate reusable context-engine attempt helpers -Today the reusable lifecycle helpers live under the PI runner: +Today the reusable lifecycle helpers live under the embedded agent runner: -- `src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts` -- `src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts` -- `src/agents/pi-embedded-runner/context-engine-maintenance.ts` +- `src/agents/embedded-agent-runner/run/attempt.context-engine-helpers.ts` +- `src/agents/embedded-agent-runner/run/attempt.prompt-helpers.ts` +- `src/agents/embedded-agent-runner/context-engine-maintenance.ts` -Codex should not import from an implementation path whose name implies PI if we -can avoid it. +Codex should import harness-neutral helpers rather than reaching into runner +implementation details. Create a harness-neutral module, for example: @@ -180,10 +180,9 @@ Move or re-export: - `buildAfterTurnRuntimeContextFromUsage` - a small wrapper around `runContextEngineMaintenance` -Keep PI imports working either by re-exporting from the old files or updating PI -call sites in the same PR. +Update built-in harness call sites in the same PR. -The neutral helper names should not mention PI. +The neutral helper names should not mention the built-in harness. Suggested names: @@ -324,10 +323,11 @@ should become context-aware: 3. run `before_prompt_build` with the projected prompt/developer instructions This order lets generic prompt hooks see the same prompt Codex will receive. If -we need strict PI parity, run context-engine assembly before hook composition, -because PI applies context-engine `systemPromptAddition` to the final system -prompt after its prompt pipeline. The important invariant is that both context -engine and hooks get a deterministic, documented order. +we need strict OpenClaw parity, run context-engine assembly before hook +composition, because the built-in harness applies context-engine +`systemPromptAddition` to the final system prompt after its prompt pipeline. The +important invariant is that both context engine and hooks get a deterministic, +documented order. Recommended order for first implementation: @@ -472,7 +472,7 @@ context-engine lifecycle currently misses reset/delete events for all harnesses. ### 10. Error handling -Follow PI semantics: +Follow built-in OpenClaw semantics: - bootstrap failures warn and continue - assemble failures warn and fall back to unassembled pipeline messages/prompt @@ -526,7 +526,7 @@ Add tests under `extensions/codex/src/app-server`: event details change. - `src/agents/harness/selection.test.ts` should not need changes unless config behavior changes; it should remain stable. -- PI context-engine tests should continue to pass unchanged. +- Built-in harness context-engine tests should continue to pass unchanged. ### Integration / live tests @@ -534,7 +534,7 @@ Add or extend live Codex harness smoke tests: - configure `plugins.slots.contextEngine` to a test engine - configure `agents.defaults.model` to a `codex/*` model -- configure `agents.defaults.embeddedHarness.runtime = "codex"` +- configure provider/model `agentRuntime.id = "codex"` - assert test engine observed: - bootstrap - assemble @@ -599,9 +599,9 @@ This should be backward-compatible: 3. Should `before_prompt_build` run before or after context-engine assembly? Recommendation: after context-engine projection for Codex, so generic harness - hooks see the actual prompt/developer instructions Codex will receive. If PI - parity requires the opposite, encode the chosen order in tests and document it - here. + hooks see the actual prompt/developer instructions Codex will receive. If + built-in harness parity requires the opposite, encode the chosen order in + tests and document it here. 4. Can Codex app-server accept a future structured context/history override? @@ -619,6 +619,6 @@ This should be backward-compatible: - Failed/aborted/yield-aborted turns do not run turn maintenance. - Context-engine-owned compaction remains primary for OpenClaw/plugin state. - Codex native compaction remains auditable as native Codex behavior. -- Existing PI context-engine behavior is unchanged. +- Existing built-in harness context-engine behavior is unchanged. - Existing Codex harness behavior is unchanged when no non-legacy context engine is selected or when assembly fails. diff --git a/docs/plugins/architecture-internals.md b/docs/plugins/architecture-internals.md index 437ff399c81..688b5cdcfda 100644 --- a/docs/plugins/architecture-internals.md +++ b/docs/plugins/architecture-internals.md @@ -263,7 +263,7 @@ listed here. | 4 | `normalizeTransport` | Normalize provider-family `api` / `baseUrl` before generic model assembly | Provider owns transport cleanup for custom provider ids in the same transport family | | 5 | `normalizeConfig` | Normalize `models.providers.` before runtime/provider resolution | Provider needs config cleanup that should live with the plugin; bundled Google-family helpers also backstop supported Google config entries | | 6 | `applyNativeStreamingUsageCompat` | Apply native streaming-usage compat rewrites to config providers | Provider needs endpoint-driven native streaming usage metadata fixes | -| 7 | `resolveConfigApiKey` | Resolve env-marker auth for config providers before runtime auth loading | Provider has provider-owned env-marker API-key resolution; `amazon-bedrock` also has a built-in AWS env-marker resolver here | +| 7 | `resolveConfigApiKey` | Resolve env-marker auth for config providers before runtime auth loading | Providers expose their own env-marker API-key resolution hooks | | 8 | `resolveSyntheticAuth` | Surface local/self-hosted or config-backed auth without persisting plaintext | Provider can operate with a synthetic/local credential marker | | 9 | `resolveExternalAuthProfiles` | Overlay provider-owned external auth profiles; default `persistence` is `runtime-only` for CLI/app-owned creds | Provider reuses external auth credentials without persisting copied refresh tokens; declare `contracts.externalAuthProviders` in the manifest | | 10 | `shouldDeferSyntheticProfileAuth` | Lower stored synthetic profile placeholders behind env/config-backed auth | Provider stores synthetic placeholder profiles that should not win precedence | @@ -280,7 +280,7 @@ listed here. | 21 | `resolveTransportTurnState` | Attach native per-turn transport headers or metadata | Provider wants generic transports to send provider-native turn identity | | 22 | `resolveWebSocketSessionPolicy` | Attach native WebSocket headers or session cool-down policy | Provider wants generic WS transports to tune session headers or fallback policy | | 23 | `formatApiKey` | Auth-profile formatter: stored profile becomes the runtime `apiKey` string | Provider stores extra auth metadata and needs a custom runtime token shape | -| 24 | `refreshOAuth` | OAuth refresh override for custom refresh endpoints or refresh-failure policy | Provider does not fit the shared `pi-ai` refreshers | +| 24 | `refreshOAuth` | OAuth refresh override for custom refresh endpoints or refresh-failure policy | Provider does not fit the shared OpenClaw refreshers | | 25 | `buildAuthDoctorHint` | Repair hint appended when OAuth refresh fails | Provider needs provider-owned auth repair guidance after refresh failure | | 26 | `matchesContextOverflowError` | Provider-owned context-window overflow matcher | Provider has raw overflow errors generic heuristics would miss | | 27 | `classifyFailoverReason` | Provider-owned failover reason classification | Provider can map raw API/transport errors to rate-limit/overload/etc | diff --git a/docs/plugins/bundles.md b/docs/plugins/bundles.md index 04142733e3b..575dfe05247 100644 --- a/docs/plugins/bundles.md +++ b/docs/plugins/bundles.md @@ -1,50 +1,34 @@ --- -summary: "Install Codex, Claude, and Cursor-compatible bundles as OpenClaw plugins" +summary: "Install and use Codex, Claude, and Cursor bundles as OpenClaw plugins" read_when: - You want to install a Codex, Claude, or Cursor-compatible bundle - - You need to know which bundle features OpenClaw executes - - You are debugging bundle detection, MCP tools, LSP defaults, or missing capabilities + - You need to understand how OpenClaw maps bundle content into native features + - You are debugging bundle detection or missing capabilities title: "Plugin bundles" -doc-schema-version: 1 --- -Plugin bundles let OpenClaw reuse compatible Codex, Claude, and Cursor plugin -layouts without loading them as native OpenClaw runtime modules. Use this page -when you have an existing bundle and need to install it, verify how OpenClaw -classified it, and understand which parts become OpenClaw skills, hooks, MCP -tools, settings, or diagnostics. +OpenClaw can install plugins from three external ecosystems: **Codex**, **Claude**, +and **Cursor**. These are called **bundles** — content and metadata packs that +OpenClaw maps into native features like skills, hooks, and MCP tools. - Bundles are not native OpenClaw plugins. Native plugins run in process and can - register OpenClaw capabilities directly. Bundles are content and metadata - packs that OpenClaw maps selectively into supported surfaces. + Bundles are **not** the same as native OpenClaw plugins. Native plugins run + in-process and can register any capability. Bundles are content packs with + selective feature mapping and a narrower trust boundary. -## Choose the right plugin format +## Why bundles exist -Use a bundle when you already have a Codex, Claude, or Cursor-compatible -package and want OpenClaw to map its supported content into skills, hook packs, -MCP tools, settings, or LSP defaults without rewriting it as a native plugin. -Build a native OpenClaw plugin when the integration must register a channel, -provider, service, HTTP route, Gateway method, plugin-owned CLI command, or -another runtime capability. +Many useful plugins are published in Codex, Claude, or Cursor format. Instead +of requiring authors to rewrite them as native OpenClaw plugins, OpenClaw +detects these formats and maps their supported content into the native feature +set. This means you can install a Claude command pack or a Codex skill bundle +and use it immediately. -| Need | Use | -| --------------------------------------------------------------------------------------- | ------------- | -| Reuse skills, command markdown, MCP config, or LSP defaults from a compatible ecosystem | Bundle | -| Execute arbitrary plugin runtime code in OpenClaw | Native plugin | -| Publish a full OpenClaw capability | Native plugin | -| Port an existing Claude or Cursor command pack | Bundle | - -See [Building plugins](/plugins/building-plugins) for native plugin authoring -and [Plugins](/tools/plugin) for the main install workflow. - -## Install and verify a bundle +## Install a bundle - - Install from a local directory, archive, or supported marketplace source: - + ```bash # Local directory openclaw plugins install ./my-bundle @@ -59,281 +43,268 @@ and [Plugins](/tools/plugin) for the main install workflow. - + ```bash openclaw plugins list openclaw plugins inspect ``` - A compatible bundle appears with `Format: bundle` and a `codex`, `claude`, - or `cursor` subtype. + Bundles show as `Format: bundle` with a subtype of `codex`, `claude`, or `cursor`. - + ```bash openclaw gateway restart ``` - Installing or updating plugin code requires restarting the Gateway. + Mapped features (skills, hooks, MCP tools, LSP defaults) are available in the next session. ## What OpenClaw maps from bundles -Not every bundle feature runs in OpenClaw today. OpenClaw maps supported content -into native surfaces and reports detect-only content in plugin diagnostics. +Not every bundle feature runs in OpenClaw today. Here is what works and what +is detected but not yet wired. ### Supported now -| Feature | How it maps | Applies to | -| ------------- | -------------------------------------------------------------------------------------------- | --------------- | -| Skill content | Bundle skill roots load as normal OpenClaw skills | All formats | -| Commands | `commands/` and `.cursor/commands/` are treated as skill roots | Claude, Cursor | -| Hook packs | OpenClaw-style `HOOK.md` and `handler.ts` or `handler.js` layouts | Primarily Codex | -| MCP tools | Bundle MCP config merges into embedded Pi settings; supported stdio and HTTP servers load | All formats | -| LSP servers | Claude `.lsp.json` and manifest-declared `lspServers` merge into embedded Pi LSP defaults | Claude | -| Settings | Claude `settings.json` imports as embedded Pi defaults after shell override keys are removed | Claude | +| Feature | How it maps | Applies to | +| ------------- | ------------------------------------------------------------------------------------------------- | -------------- | +| Skill content | Bundle skill roots load as normal OpenClaw skills | All formats | +| Commands | `commands/` and `.cursor/commands/` treated as skill roots | Claude, Cursor | +| Hook packs | OpenClaw-style `HOOK.md` + `handler.ts` layouts | Codex | +| MCP tools | Bundle MCP config merged into embedded OpenClaw settings; supported stdio and HTTP servers loaded | All formats | +| LSP servers | Claude `.lsp.json` and manifest-declared `lspServers` merged into embedded OpenClaw LSP defaults | Claude | +| Settings | Claude `settings.json` imported as embedded OpenClaw defaults | Claude | -### Skill content +#### Skill content -Bundle skill roots load as normal OpenClaw skill roots. Claude `commands/` and -Cursor `.cursor/commands/` load through the same path. +- bundle skill roots load as normal OpenClaw skill roots +- Claude `commands` roots are treated as additional skill roots +- Cursor `.cursor/commands` roots are treated as additional skill roots -### Hook packs +This means Claude markdown command files work through the normal OpenClaw skill +loader. Cursor command markdown works through the same path. -Bundle hook roots run **only** when they use the normal OpenClaw hook-pack layout: -`HOOK.md` with `handler.ts` or `handler.js`. Today this is primarily the -Codex-compatible case. +#### Hook packs -### MCP tools +- bundle hook roots work **only** when they use the normal OpenClaw hook-pack + layout. Today this is primarily the Codex-compatible case: + - `HOOK.md` + - `handler.ts` or `handler.js` -Enabled bundles can contribute MCP server config to embedded Pi as `mcpServers`. -Supported stdio and HTTP servers can expose tools during embedded Pi turns. The -`coding` and `messaging` tool profiles include bundle MCP tools by default; use -`tools.deny: ["bundle-mcp"]` to opt out for an agent or Gateway. +#### MCP for embedded OpenClaw -### Embedded Pi settings +- enabled bundles can contribute MCP server config +- OpenClaw merges bundle MCP config into the effective embedded OpenClaw settings as + `mcpServers` +- OpenClaw exposes supported bundle MCP tools during embedded OpenClaw agent turns by + launching stdio servers or connecting to HTTP servers +- the `coding` and `messaging` tool profiles include bundle MCP tools by + default; use `tools.deny: ["bundle-mcp"]` to opt out for an agent or gateway +- project-local embedded agent settings still apply after bundle defaults, so workspace + settings can override bundle MCP entries when needed +- bundle MCP tool catalogs are sorted deterministically before registration, so + upstream `listTools()` order changes do not thrash prompt-cache tool blocks -Claude `settings.json` imports as default embedded Pi settings when the bundle is -enabled. OpenClaw removes shell override keys before applying them. +##### Transports -### Embedded Pi LSP +MCP servers can use stdio or HTTP transport: -Claude `.lsp.json` and manifest-declared `lspServers` merge into embedded Pi LSP -defaults. Supported stdio-backed LSP servers can run. - -### Detected but not executed - -OpenClaw reports these in diagnostics but does not run them: - -- Claude `agents`, `hooks/hooks.json`, `outputStyles` -- Cursor `.cursor/agents`, `.cursor/hooks.json`, `.cursor/rules` -- Codex app or inline metadata - -## Bundle formats and detection - -OpenClaw checks native plugin markers before bundle markers. A directory with -`openclaw.plugin.json` or a valid `package.json` `openclaw.extensions` entry is -treated as a native plugin, even if it also contains bundle files. This prevents -dual-format packages from being partially loaded through the bundle path. - -After native detection, OpenClaw recognizes these bundle layouts: - - - - Marker: `.codex-plugin/plugin.json` - - Supported mapped content: `skills/`, `hooks/`, `.mcp.json`, and `.app.json` - capability reporting. - - Codex bundles fit OpenClaw best when they use skill roots and OpenClaw-style - hook-pack directories. - - - - - Detection modes: - - - **Manifest-based:** `.claude-plugin/plugin.json` - - **Manifestless:** default Claude layout with `skills/`, `commands/`, - `agents/`, `hooks/hooks.json`, `.mcp.json`, `.lsp.json`, or - `settings.json` - - Supported mapped content: `skills/`, `commands/`, `settings.json`, - `.mcp.json`, `.lsp.json`, manifest-declared `mcpServers`, and - manifest-declared `lspServers`. - - Detect-only content: `agents`, `hooks/hooks.json`, and `outputStyles`. - - - - - Marker: `.cursor-plugin/plugin.json` - - Supported mapped content: `skills/`, `.cursor/commands/`, and `.mcp.json`. - - Detect-only content: `.cursor/agents`, `.cursor/hooks.json`, and - `.cursor/rules`. - - - - -Claude manifest component paths are additive. Declaring custom paths extends -the default paths that exist in the bundle instead of replacing them. - -## MCP config reference - -Bundle MCP tools use the synthetic plugin key `bundle-mcp` for profile filtering. -To opt out for an agent or Gateway, deny that key: - -```json5 -{ - tools: { - deny: ["bundle-mcp"], - }, -} -``` - -Project-local embedded Pi settings still apply after bundle defaults, so -workspace settings can override bundle MCP entries when needed. - -### MCP config shape - -Bundle MCP files can use either `mcpServers`, `servers`, or a top-level server -map. Stdio servers launch a child process: +**Stdio** launches a child process: ```json { - "mcpServers": { - "my-server": { - "command": "node", - "args": ["server.js"], - "env": { "PORT": "3000" } + "mcp": { + "servers": { + "my-server": { + "command": "node", + "args": ["server.js"], + "env": { "PORT": "3000" } + } } } } ``` -HTTP servers connect over `sse` by default, or `streamable-http` when requested: +**HTTP** connects to a running MCP server over `sse` by default, or `streamable-http` when requested: ```json { - "mcpServers": { - "my-server": { - "url": "http://localhost:3100/mcp", - "transport": "streamable-http", - "headers": { - "Authorization": "Bearer local-dev-token" - }, - "connectionTimeoutMs": 30000 + "mcp": { + "servers": { + "my-server": { + "url": "http://localhost:3100/mcp", + "transport": "streamable-http", + "headers": { + "Authorization": "Bearer ${MY_SECRET_TOKEN}" + }, + "connectionTimeoutMs": 30000 + } } } } ``` -Rules: - -- `transport` may be `"sse"` or `"streamable-http"`. When omitted, OpenClaw - uses `sse`. -- `type: "http"` is a CLI-native downstream alias. Prefer - `transport: "streamable-http"` in bundle config; `openclaw mcp set` and - `openclaw doctor --fix` normalize the alias. -- Only `http:` and `https:` URLs are supported. -- `headers` must be a JSON object with string-compatible values. -- A server entry with `command` is treated as stdio. A server entry with `url` - and no command is treated as HTTP. -- URL credentials, including userinfo and query params, are redacted from tool - descriptions and logs. +- `transport` may be set to `"streamable-http"` or `"sse"`; when omitted, OpenClaw uses `sse` +- `type: "http"` is a CLI-native downstream shape; use `transport: "streamable-http"` in OpenClaw config. `openclaw mcp set` and `openclaw doctor --fix` normalize the common alias. +- only `http:` and `https:` URL schemes are allowed +- `headers` values support `${ENV_VAR}` interpolation +- a server entry with both `command` and `url` is rejected +- URL credentials (userinfo and query params) are redacted from tool + descriptions and logs - `connectionTimeoutMs` overrides the default 30-second connection timeout for - stdio and HTTP transports. + both stdio and HTTP transports -For stdio startup safety, unsupported environment-variable entries are ignored -with diagnostics instead of being passed through blindly. +##### Tool naming -### MCP paths and tool names +OpenClaw registers bundle MCP tools with provider-safe names in the form +`serverName__toolName`. For example, a server keyed `"vigil-harbor"` exposing a +`memory_search` tool registers as `vigil-harbor__memory_search`. -File-backed MCP config is resolved relative to the bundle file that declared -it. Explicit relative `command`, `args`, `cwd`, and `workingDirectory` values -are expanded against that file's directory. Claude bundle config can also use -`${CLAUDE_PLUGIN_ROOT}` to refer to the bundle root. +- characters outside `A-Za-z0-9_-` are replaced with `-` +- fragments that would start with a non-letter get a letter prefix, so numeric + server keys such as `12306` become provider-safe tool prefixes +- server prefixes are capped at 30 characters +- full tool names are capped at 64 characters +- empty server names fall back to `mcp` +- colliding sanitized names are disambiguated with numeric suffixes +- final exposed tool order is deterministic by safe name to keep repeated embedded-agent + turns cache-stable +- profile filtering treats all tools from one bundle MCP server as plugin-owned + by `bundle-mcp`, so profile allowlists and deny lists can include either + individual exposed tool names or the `bundle-mcp` plugin key -OpenClaw registers bundle MCP tools with provider-safe names: +#### Embedded OpenClaw settings -```text -serverName__toolName -``` - -Naming rules: - -- Characters outside `A-Za-z0-9_-` become `-`. -- Server prefixes must start with a letter; numeric server keys get an `mcp-` - prefix. -- Empty server names fall back to `mcp`. -- Server prefixes are capped at 30 characters. -- Full tool names are capped at 64 characters. -- Colliding sanitized names get numeric suffixes. -- Exposed tools are sorted deterministically by safe name so repeated Pi turns - keep stable tool blocks. -- Profile allowlists and denylists can name either individual exposed tools or - the `bundle-mcp` plugin key. - -## Embedded Pi settings and LSP defaults - -Enabled Claude bundles can contribute `settings.json` defaults to the embedded -Pi runtime. OpenClaw applies those settings before project-local settings, then -sanitizes shell override keys so bundle or workspace settings cannot change -shell execution behavior. +- Claude `settings.json` is imported as default embedded OpenClaw settings when the + bundle is enabled +- OpenClaw sanitizes shell override keys before applying them Sanitized keys: - `shellPath` - `shellCommandPrefix` -Enabled Claude bundles can also contribute LSP server config through `.lsp.json` -or manifest-declared `lspServers`. OpenClaw merges those entries into embedded -Pi LSP defaults. Supported stdio-backed LSP servers can run; unsupported server -entries still appear in `openclaw plugins inspect ` diagnostics. +#### Embedded OpenClaw LSP + +- enabled Claude bundles can contribute LSP server config +- OpenClaw loads `.lsp.json` plus any manifest-declared `lspServers` paths +- bundle LSP config is merged into the effective embedded OpenClaw LSP defaults +- only supported stdio-backed LSP servers are runnable today; unsupported + transports still show up in `openclaw plugins inspect ` + +### Detected but not executed + +These are recognized and shown in diagnostics, but OpenClaw does not run them: + +- Claude `agents`, `hooks.json` automation, `outputStyles` +- Cursor `.cursor/agents`, `.cursor/hooks.json`, `.cursor/rules` +- Codex inline/app metadata beyond capability reporting + +## Bundle formats + + + + Markers: `.codex-plugin/plugin.json` + + Optional content: `skills/`, `hooks/`, `.mcp.json`, `.app.json` + + Codex bundles fit OpenClaw best when they use skill roots and OpenClaw-style + hook-pack directories (`HOOK.md` + `handler.ts`). + + + + + Two detection modes: + + - **Manifest-based:** `.claude-plugin/plugin.json` + - **Manifestless:** default Claude layout (`skills/`, `commands/`, `agents/`, `hooks/`, `.mcp.json`, `.lsp.json`, `settings.json`) + + Claude-specific behavior: + + - `commands/` is treated as skill content + - `settings.json` is imported into embedded OpenClaw settings (shell override keys are sanitized) + - `.mcp.json` exposes supported stdio tools to embedded OpenClaw + - `.lsp.json` plus manifest-declared `lspServers` paths load into embedded OpenClaw LSP defaults + - `hooks/hooks.json` is detected but not executed + - Custom component paths in the manifest are additive (they extend defaults, not replace them) + + + + + Markers: `.cursor-plugin/plugin.json` + + Optional content: `skills/`, `.cursor/commands/`, `.cursor/agents/`, `.cursor/rules/`, `.cursor/hooks.json`, `.mcp.json` + + - `.cursor/commands/` is treated as skill content + - `.cursor/rules/`, `.cursor/agents/`, and `.cursor/hooks.json` are detect-only + + + + +## Detection precedence + +OpenClaw checks for native plugin format first: + +1. `openclaw.plugin.json` or valid `package.json` with `openclaw.extensions` — treated as **native plugin** +2. Bundle markers (`.codex-plugin/`, `.claude-plugin/`, or default Claude/Cursor layout) — treated as **bundle** + +If a directory contains both, OpenClaw uses the native path. This prevents +dual-format packages from being partially installed as bundles. ## Runtime dependencies and cleanup -Third-party compatible bundles do not get startup `npm install` repair. Install -them with `openclaw plugins install`, and ship every runtime file they need -inside the installed plugin directory. +- Third-party compatible bundles do not get startup `npm install` repair. They + should be installed through `openclaw plugins install` and ship everything + they need in the installed plugin directory. +- OpenClaw-owned bundled plugins are either shipped lightweight in core or + downloadable through the plugin installer. Gateway startup never runs a + package manager for them. +- `openclaw doctor --fix` removes legacy staged dependency directories and can + recover downloadable plugins that are missing from the local plugin index when + config references them. -OpenClaw-owned bundled plugins are either shipped lightweight in core or -downloadable through the plugin installer. Gateway startup does not run a -package manager for them. `openclaw doctor --fix` can remove legacy staged -dependency directories and recover downloadable plugins that config references -but the local plugin index is missing. +## Security -## Security boundary +Bundles have a narrower trust boundary than native plugins: -Bundles have a narrower runtime boundary than native plugins: +- OpenClaw does **not** load arbitrary bundle runtime modules in-process +- Skills and hook-pack paths must stay inside the plugin root (boundary-checked) +- Settings files are read with the same boundary checks +- Supported stdio MCP servers may be launched as subprocesses -- OpenClaw does not load arbitrary bundle runtime modules in process. -- Skill roots, hook-pack paths, settings files, MCP files, and LSP files are - read with plugin-root boundary checks. -- OpenClaw-style hook packs must stay inside the plugin root. -- Supported stdio MCP servers can still launch subprocesses. - -Treat third-party bundles as trusted content for the mapped features they -expose, especially MCP servers and hook packs. +This makes bundles safer by default, but you should still treat third-party +bundles as trusted content for the features they do expose. ## Troubleshooting -| Symptom | Check | Fix | -| -------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -| Capability is listed but does not run | Run `openclaw plugins inspect ` and check whether it is marked as not wired | This is a current product limit, not a broken install | -| Claude command files do not appear as skills | Check that markdown files are inside `commands/` or a declared command path | Move the files under a detected `commands/` or `skills/` root, enable the bundle, and restart | -| Claude `settings.json` does not apply | Check that the bundle is enabled and inspect diagnostics | Only embedded Pi settings are imported; shell override keys are removed | -| Claude hooks do not execute | Check whether the bundle only has `hooks/hooks.json` | Use an OpenClaw hook-pack layout or ship a native plugin | + + + Run `openclaw plugins inspect `. If a capability is listed but marked as + not wired, that is a product limit — not a broken install. + + + + Make sure the bundle is enabled and the markdown files are inside a detected + `commands/` or `skills/` root. + + + + Only embedded OpenClaw settings from `settings.json` are supported. OpenClaw does + not treat bundle settings as raw config patches. + + + + `hooks/hooks.json` is detect-only. If you need runnable hooks, use the + OpenClaw hook-pack layout or ship a native plugin. + + ## Related -- [Plugins](/tools/plugin) - install, configure, and troubleshoot plugins -- [Manage plugins](/plugins/manage-plugins) - common plugin CLI examples -- [Plugin inventory](/plugins/plugin-inventory) - generated bundled and external plugin list -- [Plugin manifest](/plugins/manifest) - native plugin manifest schema -- [Building plugins](/plugins/building-plugins) - create a native plugin +- [Install and Configure Plugins](/tools/plugin) +- [Building Plugins](/plugins/building-plugins) — create a native plugin +- [Plugin Manifest](/plugins/manifest) — native manifest schema diff --git a/docs/plugins/codex-harness-runtime.md b/docs/plugins/codex-harness-runtime.md index 54e6ff340f1..13d4adc5b6e 100644 --- a/docs/plugins/codex-harness-runtime.md +++ b/docs/plugins/codex-harness-runtime.md @@ -4,7 +4,7 @@ title: "Codex harness runtime" read_when: - You need the Codex harness runtime support contract - You are debugging native Codex tools, hooks, compaction, or feedback upload - - You are changing plugin behavior across PI and Codex harness turns + - You are changing plugin behavior across OpenClaw and Codex harness turns --- This page documents the runtime contract for Codex harness turns. For setup and @@ -13,7 +13,7 @@ see [Codex harness reference](/plugins/codex-harness-reference). ## Overview -Codex mode is not PI with a different model call underneath. Codex owns more of +Codex mode is not OpenClaw with a different model call underneath. Codex owns more of the native model loop, and OpenClaw adapts its plugin, tool, session, and diagnostic surfaces around that boundary. @@ -24,7 +24,7 @@ continuation, and native compaction. Prompt routing follows the selected runtime, not just the provider string. A native Codex turn receives Codex app-server developer instructions, while an -explicit PI compatibility route keeps the normal OpenClaw/PI system prompt even +explicit OpenClaw compatibility route keeps the normal OpenClaw system prompt even when it uses Codex-flavored OpenAI auth or transport. Native Codex keeps Codex-owned base/model instructions and project-doc behavior @@ -73,7 +73,7 @@ The Codex harness has three hook layers: | Layer | Owner | Purpose | | ------------------------------------- | ------------------------ | ------------------------------------------------------------------- | -| OpenClaw plugin hooks | OpenClaw | Product/plugin compatibility across PI and Codex harnesses. | +| OpenClaw plugin hooks | OpenClaw | Product/plugin compatibility across OpenClaw and Codex harnesses. | | Codex app-server extension middleware | OpenClaw bundled plugins | Per-turn adapter behavior around OpenClaw dynamic tools. | | Codex native hooks | Codex | Low-level Codex lifecycle and native tool policy from Codex config. | @@ -241,7 +241,7 @@ settings such as `agents.defaults.imageGenerationModel`, `videoGenerationModel`, `pdfModel`, and `messages.tts`. Text, images, video, music, TTS, approvals, and messaging-tool output continue -through the normal OpenClaw delivery path. Media generation does not require PI. +through the normal OpenClaw delivery path. Media generation does not require the legacy runtime. When Codex emits a native image-generation item with a `savedPath`, OpenClaw forwards that exact file through the normal reply-media path even if the Codex turn has no assistant text. diff --git a/docs/plugins/codex-harness.md b/docs/plugins/codex-harness.md index 68c4df349cc..f46130e7e33 100644 --- a/docs/plugins/codex-harness.md +++ b/docs/plugins/codex-harness.md @@ -4,11 +4,11 @@ title: "Codex harness" read_when: - You want to use the bundled Codex app-server harness - You need Codex harness config examples - - You want Codex-only deployments to fail instead of falling back to PI + - You want Codex-only deployments to fail instead of falling back to OpenClaw --- The bundled `codex` plugin lets OpenClaw run embedded OpenAI agent turns -through Codex app-server instead of the built-in PI harness. +through Codex app-server instead of the built-in OpenClaw harness. Use the Codex harness when you want Codex to own the low-level agent session: native thread resume, native tool continuation, native compaction, and @@ -115,7 +115,7 @@ harness options in OpenClaw config, and use the CLI only for Codex auth: | Sign in with Codex OAuth | `openclaw models auth login --provider openai-codex` | CLI auth profile | | Add API-key backup for Codex runs | `openai:*` API-key profile listed after subscription auth in `auth.order.openai` | CLI auth profile + OpenClaw config | | Fail closed when Codex is unavailable | Provider or model `agentRuntime.id: "codex"` | OpenClaw model/provider config | -| Use direct OpenAI API traffic | Provider or model `agentRuntime.id: "pi"` with normal OpenAI auth | OpenClaw model/provider config | +| Use direct OpenAI API traffic | Provider or model `agentRuntime.id: "openclaw"` with normal OpenAI auth | OpenClaw model/provider config | | Tune app-server behavior | `plugins.entries.codex.config.appServer.*` | Codex plugin config | | Enable native Codex plugin apps | `plugins.entries.codex.config.codexPlugins.*` | Codex plugin config | | Enable Codex Computer Use | `plugins.entries.codex.config.computerUse.*` | Codex plugin config | @@ -160,7 +160,7 @@ instead of silently switching compaction backends. ``` In that shape, both profiles still run through Codex for `openai/gpt-*` agent -turns. The API key is only an auth fallback, not a request to switch to PI or +turns. The API key is only an auth fallback, not a request to switch to OpenClaw or plain OpenAI Responses. The rest of this page covers common variants users must choose between: @@ -199,8 +199,8 @@ Keep provider refs and runtime policy separate: repair legacy refs and stale session route pins. - `agentRuntime.id: "codex"` is optional for normal OpenAI auto mode, but useful when a deployment should fail closed if Codex is unavailable. -- `agentRuntime.id: "pi"` opts a provider or model into direct PI behavior when - that is intentional. +- `agentRuntime.id: "openclaw"` opts a provider or model into the OpenClaw + embedded runtime when that is intentional. - `/codex ...` controls native Codex app-server conversations from chat. - ACP/acpx is a separate external harness path. Use it only when the user asks for ACP/acpx or an external harness adapter. @@ -218,13 +218,13 @@ Common command routing: | Send Codex feedback only | `/codex diagnostics [note]` | | Start an ACP/acpx task | ACP/acpx session commands, not `/codex` | -| Use case | Configure | Verify | Notes | -| ---------------------------------------------------- | ---------------------------------------------------------------- | --------------------------------------- | ---------------------------------- | -| ChatGPT/Codex subscription with native Codex runtime | `openai/gpt-*` plus enabled `codex` plugin | `/status` shows `Runtime: OpenAI Codex` | Recommended path | -| Fail closed if Codex is unavailable | Provider or model `agentRuntime.id: "codex"` | Turn fails instead of PI fallback | Use for Codex-only deployments | -| Direct OpenAI API-key traffic through PI | Provider or model `agentRuntime.id: "pi"` and normal OpenAI auth | `/status` shows PI runtime | Use only when PI is intentional | -| Legacy config | `openai-codex/gpt-*` | `openclaw doctor --fix` rewrites it | Do not write new config this way | -| ACP/acpx Codex adapter | ACP `sessions_spawn({ runtime: "acp" })` | ACP task/session status | Separate from native Codex harness | +| Use case | Configure | Verify | Notes | +| ---------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------- | ------------------------------------- | +| ChatGPT/Codex subscription with native Codex runtime | `openai/gpt-*` plus enabled `codex` plugin | `/status` shows `Runtime: OpenAI Codex` | Recommended path | +| Fail closed if Codex is unavailable | Provider or model `agentRuntime.id: "codex"` | Turn fails instead of embedded fallback | Use for Codex-only deployments | +| Direct OpenAI API-key traffic through OpenClaw | Provider or model `agentRuntime.id: "openclaw"` and normal OpenAI auth | `/status` shows OpenClaw runtime | Use only when OpenClaw is intentional | +| Legacy config | `openai-codex/gpt-*` | `openclaw doctor --fix` rewrites it | Do not write new config this way | +| ACP/acpx Codex adapter | ACP `sessions_spawn({ runtime: "acp" })` | ACP task/session status | Separate from native Codex harness | `agents.defaults.imageModel` follows the same prefix split. Use `openai/gpt-*` for the normal OpenAI route and `codex/gpt-*` only when image understanding @@ -599,7 +599,7 @@ does not translate Codex plugins into synthetic `codex_plugin_*` OpenClaw dynamic tools. `codexPlugins` affects only sessions that select the native Codex harness. It -has no effect on PI runs, normal OpenAI provider runs, ACP conversation +has no effect on built-in harness runs, normal OpenAI provider runs, ACP conversation bindings, or other harnesses. Minimal migrated config: @@ -677,11 +677,11 @@ new configs. Select an `openai/gpt-*` model, enable `plugins.entries.codex.enabled`, and check whether `plugins.allow` excludes `codex`. -**OpenClaw uses PI instead of Codex:** make sure the model ref is +**OpenClaw uses the built-in harness instead of Codex:** make sure the model ref is `openai/gpt-*` on the official OpenAI provider and that the Codex plugin is installed and enabled. If you need strict proof while testing, set provider or model `agentRuntime.id: "codex"`. A forced Codex runtime fails instead of -falling back to PI. +falling back to OpenClaw. **OpenAI Codex runtime falls back to the API-key path:** collect a redacted gateway excerpt that shows the model, runtime, selected provider, and failure. @@ -689,7 +689,7 @@ Ask affected collaborators to run this read-only command on their OpenClaw host: ```bash ( - pattern='openai/gpt-5\.[45]|agentRuntime(\.id)?|harnessRuntime|Runtime: OpenAI Codex|openai-codex|resolveSelectedOpenAIPiRuntimeProvider|candidateProvider[": ]+openai|status[": ]+401|Incorrect API key|No API key|api-key path|API-key path|OAuth' + pattern='openai/gpt-5\.[45]|agentRuntime(\.id)?|harnessRuntime|Runtime: OpenAI Codex|openai-codex|resolveSelectedOpenAIRuntimeProvider|candidateProvider[": ]+openai|status[": ]+401|Incorrect API key|No API key|api-key path|API-key path|OAuth' if ls /tmp/openclaw/openclaw-*.log >/dev/null 2>&1; then grep -E -i -n "$pattern" /tmp/openclaw/openclaw-*.log 2>/dev/null || true @@ -734,9 +734,9 @@ that any custom `appServer.command`, `url`, `authToken`, or headers are valid. headers, and that the remote app-server speaks the same Codex app-server protocol version. -**A non-Codex model uses PI:** that is expected unless provider or model runtime -policy routes it to another harness. Plain non-OpenAI provider refs stay on -their normal provider path in `auto` mode. +**A non-Codex model uses the built-in harness:** that is expected unless +provider or model runtime policy routes it to another harness. Plain non-OpenAI +provider refs stay on their normal provider path in `auto` mode. **Computer Use is installed but tools do not run:** check `/codex computer-use status` from a fresh session. If a tool reports diff --git a/docs/plugins/codex-native-plugins.md b/docs/plugins/codex-native-plugins.md index 1716930e320..0e7756de53e 100644 --- a/docs/plugins/codex-native-plugins.md +++ b/docs/plugins/codex-native-plugins.md @@ -27,7 +27,7 @@ Use this page after the base [Codex harness](/plugins/codex-harness) is working. - The target Codex app-server must be able to see the expected marketplace, plugin, and app inventory. -`codexPlugins` has no effect on PI runs, normal OpenAI provider runs, ACP +`codexPlugins` has no effect on OpenClaw runs, normal OpenAI provider runs, ACP conversation bindings, or other harnesses because those paths do not create Codex app-server threads with native `apps` config. diff --git a/docs/plugins/manifest.md b/docs/plugins/manifest.md index 23238a1307b..09aa36b399d 100644 --- a/docs/plugins/manifest.md +++ b/docs/plugins/manifest.md @@ -617,7 +617,7 @@ read without importing the plugin runtime. ```json { "contracts": { - "agentToolResultMiddleware": ["pi", "codex"], + "agentToolResultMiddleware": ["openclaw", "codex"], "externalAuthProviders": ["acme-ai"], "embeddingProviders": ["openai-compatible"], "speechProviders": ["openai"], diff --git a/docs/plugins/sdk-agent-harness.md b/docs/plugins/sdk-agent-harness.md index 615e7542a92..655e5900485 100644 --- a/docs/plugins/sdk-agent-harness.md +++ b/docs/plugins/sdk-agent-harness.md @@ -47,7 +47,7 @@ That split is intentional. A harness runs a prepared attempt; it does not pick providers, replace channel delivery, or silently switch models. The prepared attempt also includes `params.runtimePlan`, an OpenClaw-owned -policy bundle for runtime decisions that must stay shared across PI and native +policy bundle for runtime decisions that must stay shared across OpenClaw and native harnesses: - `runtimePlan.tools.normalize(...)` and @@ -59,7 +59,7 @@ harnesses: - `runtimePlan.outcome.classifyRunResult(...)` for model fallback classification - `runtimePlan.observability` for resolved provider/model/harness metadata -Harnesses may use the plan for decisions that need to match PI behavior, but +Harnesses may use the plan for decisions that need to match OpenClaw behavior, but should still treat it as host-owned attempt state. Do not mutate it or use it to switch providers/models inside a turn. @@ -107,14 +107,13 @@ OpenClaw chooses a harness after provider/model resolution: 2. Provider-scoped runtime policy comes next. 3. `auto` asks registered harnesses if they support the resolved provider/model. -4. If no registered harness matches, OpenClaw uses PI unless PI fallback is - disabled. +4. If no registered harness matches, OpenClaw uses its embedded runtime. -Plugin harness failures surface as run failures. In `auto` mode, PI fallback is +Plugin harness failures surface as run failures. In `auto` mode, embedded fallback is only used when no registered plugin harness supports the resolved provider/model. Once a plugin harness has claimed a run, OpenClaw does not -replay that same turn through PI because that can change auth/runtime semantics -or duplicate side effects. +replay that same turn through another runtime because that can change +auth/runtime semantics or duplicate side effects. Whole-session and whole-agent runtime pins are ignored by selection. That includes stale session `agentHarnessId` values, `agents.defaults.agentRuntime`, @@ -164,14 +163,14 @@ Codex `0.124.0`, while pinning OpenClaw to the newer tested stable line. Bundled plugins can attach runtime-neutral tool-result middleware through `api.registerAgentToolResultMiddleware(...)` when their manifest declares the targeted runtime ids in `contracts.agentToolResultMiddleware`. This trusted -seam is for async tool-result transforms that must run before PI or Codex feeds +seam is for async tool-result transforms that must run before OpenClaw or Codex feeds tool output back into the model. Legacy bundled plugins can still use `api.registerCodexAppServerExtensionFactory(...)` for Codex app-server-only middleware, but new result transforms should use the runtime-neutral API. -The Pi-only `api.registerEmbeddedExtensionFactory(...)` hook has been removed; -Pi tool-result transforms must use runtime-neutral middleware. +The embedded-runner-only `api.registerEmbeddedExtensionFactory(...)` hook has been removed; +embedded tool-result transforms must use runtime-neutral middleware. ### Terminal outcome classification @@ -199,17 +198,17 @@ visible transcript mirror, tool policy, approvals, media delivery, and session selection. Use provider/model `agentRuntime.id: "codex"` when you need to prove that only the Codex app-server path can claim the run. Explicit plugin runtimes fail closed; Codex app-server selection failures and runtime failures are not -retried through PI. +retried through another runtime. ## Runtime strictness By default, OpenClaw uses `auto` provider/model runtime policy: registered -plugin harnesses can claim a provider/model pair, and PI handles the turn when -none match. OpenAI agent refs on the official OpenAI provider default to Codex. +plugin harnesses can claim a provider/model pair, and the embedded runtime +handles the turn when none match. OpenAI agent refs on the official OpenAI provider default to Codex. Use an explicit provider/model plugin runtime such as `agentRuntime.id: "codex"` when missing harness selection should fail instead -of routing through PI. Selected plugin harness failures always fail hard. This -does not block an explicit provider/model `agentRuntime.id: "pi"`. +of routing through the embedded runtime. Selected plugin harness failures always +fail hard. This does not block an explicit provider/model `agentRuntime.id: "openclaw"`. For Codex-only embedded runs: @@ -305,7 +304,7 @@ The OpenClaw transcript remains the compatibility layer for: - channel-visible session history - transcript search and indexing -- switching back to the built-in PI harness on a later turn +- switching back to the built-in OpenClaw harness on a later turn - generic `/new`, `/reset`, and session deletion behavior If your harness stores a sidecar binding, implement `reset(...)` so OpenClaw can @@ -318,12 +317,12 @@ When a harness executes a dynamic tool call, return the tool result back through the harness result shape instead of sending channel media yourself. This keeps text, image, video, music, TTS, approval, and messaging-tool outputs -on the same delivery path as PI-backed runs. +on the same delivery path as OpenClaw-backed runs. ## Current limitations - The public import path is generic, but some attempt/result type aliases still - carry `Pi` names for compatibility. + carry legacy names for compatibility. - Third-party harness installation is experimental. Prefer provider plugins until you need a native session runtime. - Harness switching is supported across turns. Do not switch harnesses in the diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 733edfb692b..d24015811d2 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -30,13 +30,13 @@ anything they needed from a single entry point: window. - **`openclaw/extension-api`** - a bridge that gave plugins direct access to host-side helpers like the embedded agent runner. -- **`api.registerEmbeddedExtensionFactory(...)`** - a removed Pi-only bundled +- **`api.registerEmbeddedExtensionFactory(...)`** - a removed embedded-runner-only bundled extension hook that could observe embedded-runner events such as `tool_result`. The broad import surfaces are now **deprecated**. They still work at runtime, but new plugins must not use them, and existing plugins should migrate before -the next major release removes them. The Pi-only embedded extension factory +the next major release removes them. The embedded-runner-only extension factory registration API has been removed; use tool-result middleware instead. OpenClaw does not remove or reinterpret documented plugin behavior in the same @@ -48,7 +48,7 @@ registration behavior. The backwards-compatibility layer will be removed in a future major release. Plugins that still import from these surfaces will break when that happens. - Pi-only embedded extension factory registrations already no longer load. + Legacy embedded extension factory registrations already no longer load. ## Why this changed @@ -294,17 +294,17 @@ releases. - - Bundled plugins must replace Pi-only + + Bundled plugins must replace embedded-runner-only `api.registerEmbeddedExtensionFactory(...)` tool-result handlers with runtime-neutral middleware. ```typescript - // Pi and Codex runtime dynamic tools + // OpenClaw and Codex runtime dynamic tools api.registerAgentToolResultMiddleware(async (event) => { return compactToolResult(event); }, { - runtimes: ["pi", "codex"], + runtimes: ["openclaw", "codex"], }); ``` @@ -313,7 +313,7 @@ releases. ```json { "contracts": { - "agentToolResultMiddleware": ["pi", "codex"] + "agentToolResultMiddleware": ["openclaw", "codex"] } } ``` @@ -406,11 +406,11 @@ releases. ```typescript // Before (deprecated extension-api bridge) - import { runEmbeddedPiAgent } from "openclaw/extension-api"; - const result = await runEmbeddedPiAgent({ sessionId, prompt }); + import { runEmbeddedAgent } from "openclaw/extension-api"; + const result = await runEmbeddedAgent({ sessionId, prompt }); // After (injected runtime) - const result = await api.runtime.agent.runEmbeddedPiAgent({ sessionId, prompt }); + const result = await api.runtime.agent.runEmbeddedAgent({ sessionId, prompt }); ``` The same pattern applies to other legacy bridge helpers: @@ -900,8 +900,8 @@ canonical replacement. - Covered in "How to migrate → Migrate Pi tool-result extensions to - middleware" above. Included here for completeness: the removed Pi-only + Covered in "How to migrate → Migrate embedded tool-result extensions to + middleware" above. Included here for completeness: the removed embedded-runner-only `api.registerEmbeddedExtensionFactory(...)` path is replaced by `api.registerAgentToolResultMiddleware(...)` with an explicit runtime list in `contracts.agentToolResultMiddleware`. diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index 62935489755..745439c2bd6 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -133,14 +133,15 @@ structured entries: ```ts agentPromptGuidance: [ "Global command hint.", - { text: "Only show this in the main PI prompt.", surfaces: ["pi_main"] }, + { text: "Only show this in the main OpenClaw prompt.", surfaces: ["openclaw_main"] }, ]; ``` -Structured `surfaces` may include `pi_main`, `codex_app_server`, `cli_backend`, -`acp_backend`, or `subagent`. Omit `surfaces` for intentional all-surface -guidance. Do not pass an empty `surfaces` array; it is rejected so accidental -scope loss does not become global prompt text. +Structured `surfaces` may include `openclaw_main`, `codex_app_server`, +`cli_backend`, `acp_backend`, or `subagent`. `pi_main` remains a deprecated alias +for `openclaw_main`. Omit `surfaces` for intentional all-surface guidance. Do +not pass an empty `surfaces` array; it is rejected so accidental scope loss does +not become global prompt text. Native Codex app-server developer instructions are stricter than other prompt surfaces: only guidance explicitly scoped to `codex_app_server` is promoted into @@ -254,9 +255,9 @@ Examples of non-Plan consumers: seam for async output reducers such as tokenjuice. Bundled plugins must declare `contracts.agentToolResultMiddleware` for each -targeted runtime, for example `["pi", "codex"]`. External plugins +targeted runtime, for example `["openclaw", "codex"]`. External plugins cannot register this middleware; keep normal OpenClaw plugin hooks for work -that does not need pre-model tool-result timing. The old Pi-only embedded +that does not need pre-model tool-result timing. The old embedded-runner-only extension factory registration path has been removed. diff --git a/docs/plugins/sdk-provider-plugins.md b/docs/plugins/sdk-provider-plugins.md index 45337814bd2..6c7f1fcf601 100644 --- a/docs/plugins/sdk-provider-plugins.md +++ b/docs/plugins/sdk-provider-plugins.md @@ -500,7 +500,7 @@ API key auth, and dynamic model resolution. Runtime fallback notes: - `normalizeConfig` checks the matched provider first, then other hook-capable provider plugins until one actually changes the config. If no provider hook rewrites a supported Google-family config entry, the bundled Google config normalizer still applies. - - `resolveConfigApiKey` uses the provider hook when exposed. The bundled `amazon-bedrock` path also has a built-in AWS env-marker resolver here, even though Bedrock runtime auth itself still uses the AWS SDK default chain. + - `resolveConfigApiKey` uses the provider hook when exposed. Amazon Bedrock keeps AWS env-marker resolution in its provider plugin; runtime auth itself still uses the AWS SDK default chain when configured with `auth: "aws-sdk"`. - `resolveSystemPromptContribution` lets a provider inject cache-aware system-prompt guidance for a model family. Prefer it over `before_prompt_build` when the behavior belongs to one provider/model family and should preserve the stable/dynamic cache split. For detailed descriptions and real-world examples, see [Internals: Provider Runtime Hooks](/plugins/architecture-internals#provider-runtime-hooks). diff --git a/docs/plugins/sdk-runtime.md b/docs/plugins/sdk-runtime.md index 5f5fb080ca8..3576776ee75 100644 --- a/docs/plugins/sdk-runtime.md +++ b/docs/plugins/sdk-runtime.md @@ -144,7 +144,7 @@ two-party event loops that do not go through the shared inbound reply runner. `runEmbeddedAgent(...)` is the neutral helper for starting a normal OpenClaw agent turn from plugin code. It uses the same provider/model resolution and agent-harness selection as channel-triggered replies. - `runEmbeddedPiAgent(...)` remains as a compatibility alias. + `runEmbeddedPiAgent(...)` remains as a deprecated compatibility alias for existing plugins. New code should use `runEmbeddedAgent(...)`. `resolveThinkingPolicy(...)` returns the provider/model's supported thinking levels and optional default. Provider plugins own the model-specific profile through their thinking hooks, so tool plugins should call this runtime helper instead of importing or duplicating provider lists. diff --git a/docs/providers/bedrock.md b/docs/providers/bedrock.md index 82c5a8af9e8..cb82846c9f7 100644 --- a/docs/providers/bedrock.md +++ b/docs/providers/bedrock.md @@ -6,7 +6,7 @@ read_when: title: "Amazon Bedrock" --- -OpenClaw can use **Amazon Bedrock** models via pi-ai's **Bedrock Converse** +OpenClaw can use **Amazon Bedrock** models via its **Bedrock Converse** streaming provider. Bedrock auth uses the **AWS SDK default credential chain**, not an API key. diff --git a/docs/providers/openai.md b/docs/providers/openai.md index 8823f4ef862..b161fa690b7 100644 --- a/docs/providers/openai.md +++ b/docs/providers/openai.md @@ -36,9 +36,9 @@ changing config. | ---------------------------------------------------- | -------------------------------------------------------- | --------------------------------------------------------------------- | | ChatGPT/Codex subscription with native Codex runtime | `openai/gpt-5.5` | Default OpenAI agent setup. Sign in with Codex auth. | | Direct API-key billing for agent models | `openai/gpt-5.5` plus a Codex-compatible API-key profile | Use `auth.order.openai` to place the backup after subscription auth. | -| Direct API-key billing through explicit PI | `openai/gpt-5.5` plus provider/model runtime `pi` | Select a normal `openai` API-key profile. | +| Direct API-key billing through explicit OpenClaw | `openai/gpt-5.5` plus provider/model runtime `openclaw` | Select a normal `openai` API-key profile. | | Latest ChatGPT Instant API alias | `openai/chat-latest` | Direct API-key only. Moving alias for experiments, not the default. | -| ChatGPT/Codex subscription auth through explicit PI | `openai/gpt-5.5` plus provider/model runtime `pi` | Select an `openai-codex` auth profile for the compatibility route. | +| ChatGPT/Codex subscription auth through OpenClaw | `openai/gpt-5.5` plus provider/model runtime `openclaw` | Select an `openai-codex` auth profile for the compatibility route. | | Image generation or editing | `openai/gpt-image-2` | Works with either `OPENAI_API_KEY` or OpenAI Codex OAuth. | | Transparent-background images | `openai/gpt-image-1.5` | Use `outputFormat=png` or `webp` and `openai.background=transparent`. | @@ -71,11 +71,11 @@ direct API-key auth for an OpenAI agent model. OpenAI agent model turns require the bundled Codex app-server plugin. Explicit -PI runtime config remains available as an opt-in compatibility route. When PI is +OpenClaw runtime config remains available as an opt-in compatibility route. When OpenClaw is explicitly selected with an `openai-codex` auth profile, OpenClaw keeps the -public model ref as `openai/*` and routes PI internally through the legacy -Codex-auth transport. Run `openclaw doctor --fix` to repair stale -`openai-codex/*`, `codex-cli/*`, or old PI session pins that do not come from +public model ref as `openai/*` and routes internally through the Codex-auth +transport. Run `openclaw doctor --fix` to repair stale +`openai-codex/*`, `codex-cli/*`, or old runtime session pins that do not come from explicit runtime config. @@ -178,7 +178,7 @@ Choose your preferred auth method and follow the setup steps. | ---------------------- | -------------------------- | --------------------------- | ---------------- | | `openai/gpt-5.5` | omitted / provider/model `agentRuntime.id: "codex"` | Codex app-server harness | Codex-compatible OpenAI profile | | `openai/gpt-5.4-mini` | omitted / provider/model `agentRuntime.id: "codex"` | Codex app-server harness | Codex-compatible OpenAI profile | - | `openai/gpt-5.5` | provider/model `agentRuntime.id: "pi"` | PI embedded runtime | `openai` profile or selected `openai-codex` profile | + | `openai/gpt-5.5` | provider/model `agentRuntime.id: "openclaw"` | OpenClaw embedded runtime | `openai` profile or selected `openai-codex` profile | `openai/*` agent models use the Codex app-server harness. To use API-key @@ -265,13 +265,13 @@ Choose your preferred auth method and follow the setup steps. | Model ref | Runtime config | Route | Auth | |-----------|----------------|-------|------| | `openai/gpt-5.5` | omitted / provider/model `agentRuntime.id: "codex"` | Native Codex app-server harness | Codex sign-in or ordered `openai` auth profile | - | `openai/gpt-5.5` | provider/model `agentRuntime.id: "pi"` | PI embedded runtime with internal Codex-auth transport | Selected `openai-codex` profile | + | `openai/gpt-5.5` | provider/model `agentRuntime.id: "openclaw"` | OpenClaw embedded runtime with internal Codex-auth transport | Selected `openai-codex` profile | | `openai-codex/gpt-5.5` | repaired by doctor | Legacy route rewritten to `openai/gpt-5.5` | Existing `openai-codex` profile | | `codex-cli/gpt-5.5` | repaired by doctor | Legacy CLI route rewritten to `openai/gpt-5.5` | Codex app-server auth | Prefer `openai/gpt-5.5` for new subscription-backed agent config. Older - `openai-codex/gpt-*` refs are legacy PI routes, not the native Codex runtime + `openai-codex/gpt-*` refs are legacy OpenClaw routes, not the native Codex runtime path; run `openclaw doctor --fix` when you want to migrate them to canonical `openai/*` refs. `openai-codex/gpt-5.3-codex-spark` is the exception for accounts whose Codex catalog advertises that model; direct `openai/*` and @@ -345,7 +345,7 @@ Choose your preferred auth method and follow the setup steps. openclaw models auth list --agent --provider openai-codex ``` - If an older config still has `openai-codex/gpt-*` or a stale OpenAI PI + If an older config still has `openai-codex/gpt-*` or a stale OpenAI runtime session pin without explicit runtime config, repair it: ```bash @@ -377,14 +377,14 @@ Choose your preferred auth method and follow the setup steps. Chat `/status` shows which model runtime is active for the current session. The bundled Codex app-server harness appears as `Runtime: OpenAI Codex` for - OpenAI agent model turns. Stale PI session pins are repaired to Codex unless - config explicitly pins PI. + OpenAI agent model turns. Stale OpenAI runtime session pins are repaired to Codex unless + config explicitly pins OpenClaw. ### Doctor warning - If `openai-codex/*` routes or stale OpenAI PI pins remain in config or + If `openai-codex/*` routes or stale OpenAI runtime pins remain in config or session state, `openclaw doctor --fix` rewrites them to `openai/*` with the - Codex runtime unless PI is explicitly configured. + Codex runtime unless OpenClaw is explicitly configured. ### Context window cap @@ -571,7 +571,7 @@ See [Video Generation](/tools/video-generation) for shared tool parameters, prov ## GPT-5 prompt contribution -OpenClaw adds a shared GPT-5 prompt contribution for GPT-5-family runs on OpenClaw-assembled prompt surfaces. It applies by model id, so PI/provider routes such as legacy pre-repair refs (`openai-codex/gpt-5.5`), `openrouter/openai/gpt-5.5`, `opencode/gpt-5.5`, and other compatible GPT-5 refs receive the same overlay. Older GPT-4.x models do not. +OpenClaw adds a shared GPT-5 prompt contribution for GPT-5-family runs on OpenClaw-assembled prompt surfaces. It applies by model id, so OpenClaw/provider routes such as legacy pre-repair refs (`openai-codex/gpt-5.5`), `openrouter/openai/gpt-5.5`, `opencode/gpt-5.5`, and other compatible GPT-5 refs receive the same overlay. Older GPT-4.x models do not. The bundled native Codex harness does not receive this OpenClaw GPT-5 overlay through Codex app-server developer instructions. Native Codex keeps Codex-owned base, model, and project-doc behavior, while OpenClaw disables Codex's built-in personality for native threads so agent workspace personality files stay authoritative. OpenClaw contributes only runtime context such as channel delivery, OpenClaw dynamic tools, ACP delegation, workspace context, and OpenClaw skills. @@ -961,13 +961,13 @@ the Server-side compaction accordion below. - For direct OpenAI Responses models (`openai/*` on `api.openai.com`), the OpenAI plugin's Pi-harness stream wrapper auto-enables server-side compaction: + For direct OpenAI Responses models (`openai/*` on `api.openai.com`), the OpenAI plugin's OpenClaw stream wrapper auto-enables server-side compaction: - Forces `store: true` (unless model compat sets `supportsStore: false`) - Injects `context_management: [{ type: "compaction", compact_threshold: ... }]` - Default `compact_threshold`: 70% of `contextWindow` (or `80000` when unavailable) - This applies to the built-in Pi harness path and to OpenAI provider hooks used by embedded runs. The native Codex app-server harness manages its own context through Codex and is configured by OpenAI's default agent route or provider/model runtime policy. + This applies to the built-in OpenClaw runtime path and to OpenAI provider hooks used by embedded runs. The native Codex app-server harness manages its own context through Codex and is configured by OpenAI's default agent route or provider/model runtime policy. @@ -1035,7 +1035,7 @@ the Server-side compaction accordion below. { agents: { defaults: { - embeddedPi: { executionContract: "strict-agentic" }, + embeddedAgent: { executionContract: "strict-agentic" }, }, }, } diff --git a/docs/providers/opencode-go.md b/docs/providers/opencode-go.md index ed390b0ca4a..f2fab917500 100644 --- a/docs/providers/opencode-go.md +++ b/docs/providers/opencode-go.md @@ -18,7 +18,7 @@ provider id `opencode-go` so upstream per-model routing stays correct. ## Built-in catalog -OpenClaw sources most Go catalog rows from the bundled pi model registry and +OpenClaw sources most Go catalog rows from the bundled OpenClaw model registry and supplements current upstream rows while the registry catches up. Run `openclaw models list --provider opencode-go` for the current model list. diff --git a/docs/reference/AGENTS.default.md b/docs/reference/AGENTS.default.md index 13892fd4fd1..4550ee3cc30 100644 --- a/docs/reference/AGENTS.default.md +++ b/docs/reference/AGENTS.default.md @@ -91,7 +91,7 @@ git commit -m "Add Clawd workspace" ## What OpenClaw does -- Runs WhatsApp gateway + Pi coding agent so the assistant can read/write chats, fetch context, and run skills via the host Mac. +- Runs WhatsApp gateway + embedded OpenClaw agent so the assistant can read/write chats, fetch context, and run skills via the host Mac. - macOS app manages permissions (screen recording, notifications, microphone) and exposes the `openclaw` CLI via its bundled binary. - Direct chats collapse into the agent's `main` session by default; groups stay isolated as `agent:::group:` (rooms/channels: `agent:::channel:`); heartbeats keep background tasks alive. diff --git a/docs/reference/code-mode.md b/docs/reference/code-mode.md index b150e608cd0..d126f4b49f0 100644 --- a/docs/reference/code-mode.md +++ b/docs/reference/code-mode.md @@ -498,7 +498,7 @@ This prevents recursion and keeps the model-facing contract narrow. ## Tool Search interaction -Code mode supersedes the PI Tool Search model surface for runs where it is +Code mode supersedes the OpenClaw Tool Search model surface for runs where it is active. When `tools.codeMode.enabled` is true and code mode activates: @@ -511,7 +511,7 @@ When `tools.codeMode.enabled` is true and code mode activates: - Nested calls dispatch through the same OpenClaw executor path that Tool Search uses. -The existing [Tool Search](/tools/tool-search) page describes the PI compact +The existing [Tool Search](/tools/tool-search) page describes the OpenClaw compact catalog bridge. Code mode is the generic OpenClaw alternative for runs that can use `exec` and `wait`. diff --git a/docs/reference/session-management-compaction.md b/docs/reference/session-management-compaction.md index 40fbb514460..1f2c897bfcc 100644 --- a/docs/reference/session-management-compaction.md +++ b/docs/reference/session-management-compaction.md @@ -163,7 +163,7 @@ Rules of thumb: - **Daily reset** (default 4:00 AM local time on the gateway host) creates a new `sessionId` on the next message after the reset boundary. - **Idle expiry** (`session.reset.idleMinutes` or legacy `session.idleMinutes`) creates a new `sessionId` when a message arrives after the idle window. When daily + idle are both configured, whichever expires first wins. - **System events** (heartbeat, cron wakeups, exec notifications, gateway bookkeeping) may mutate the session row but do not extend daily/idle reset freshness. Reset rollover discards queued system-event notices for the previous session before the fresh prompt is built. -- **Parent fork policy** uses PI's active branch when creating a thread or subagent fork. If that branch is too large, OpenClaw starts the child with isolated context instead of failing or inheriting unusable history. The sizing policy is automatic; legacy `session.parentForkMaxTokens` config is removed by `openclaw doctor --fix`. +- **Parent fork policy** uses OpenClaw's active branch when creating a thread or subagent fork. If that branch is too large, OpenClaw starts the child with isolated context instead of failing or inheriting unusable history. The sizing policy is automatic; legacy `session.parentForkMaxTokens` config is removed by `openclaw doctor --fix`. Implementation detail: the decision happens in `initSessionState()` in `src/auto-reply/reply/session.ts`. @@ -204,7 +204,7 @@ The store is safe to edit, but the Gateway is the authority: it may rewrite or r ## Transcript structure (`*.jsonl`) -Transcripts are managed by `@earendil-works/pi-coding-agent`'s `SessionManager`. +Transcripts are managed by `openclaw/plugin-sdk/agent-sessions`'s `SessionManager`. The file is JSONL: @@ -269,9 +269,9 @@ assistant tool calls paired with their matching `toolResult` entries. --- -## When auto-compaction happens (Pi runtime) +## When auto-compaction happens (OpenClaw runtime) -In the embedded Pi agent, auto-compaction triggers in two cases: +In the embedded OpenClaw agent, auto-compaction triggers in two cases: 1. **Overflow recovery**: the model returns a context overflow error (`request_too_large`, `context length exceeded`, `input exceeds the maximum @@ -296,7 +296,7 @@ Where: - `contextWindow` is the model's context window - `reserveTokens` is headroom reserved for prompts + the next model output -These are Pi runtime semantics (OpenClaw consumes the events, but Pi decides when to compact). +These are OpenClaw runtime semantics. OpenClaw can also trigger a preflight local compaction before opening the next run when `agents.defaults.compaction.maxActiveTranscriptBytes` is set and the @@ -305,25 +305,25 @@ reopen cost, not raw archival: OpenClaw still runs normal semantic compaction, and it requires `truncateAfterCompaction` so the compacted summary can become a new successor transcript. -For embedded Pi runs, `agents.defaults.compaction.midTurnPrecheck.enabled: true` +For embedded OpenClaw runs, `agents.defaults.compaction.midTurnPrecheck.enabled: true` adds an opt-in tool-loop guard. After a tool result is appended and before the next model call, OpenClaw estimates the prompt pressure using the same preflight budget logic used at turn start. If the context no longer fits, the guard does -not compact inside Pi's `transformContext` hook. It raises a structured +not compact inside OpenClaw runtime's `transformContext` hook. It raises a structured mid-turn precheck signal, stops the current prompt submission, and lets the outer run loop use the existing recovery path: truncate oversized tool results when that is enough, or trigger the configured compaction mode and retry. The option is disabled by default and works with both `default` and `safeguard` compaction modes, including provider-backed safeguard compaction. This is independent of `maxActiveTranscriptBytes`: the byte-size guard runs -before a turn opens, while mid-turn precheck runs later in the embedded Pi tool +before a turn opens, while mid-turn precheck runs later in the embedded OpenClaw tool loop after new tool results have been appended. --- ## Compaction settings (`reserveTokens`, `keepRecentTokens`) -Pi's compaction settings live in Pi settings: +OpenClaw runtime's compaction settings live in agent settings: ```json5 { @@ -342,7 +342,7 @@ OpenClaw also enforces a safety floor for embedded runs: - Set `agents.defaults.compaction.reserveTokensFloor: 0` to disable the floor. - If it's already higher, OpenClaw leaves it alone. - Manual `/compact` honors an explicit `agents.defaults.compaction.keepRecentTokens` - and keeps Pi's recent-tail cut point. Without an explicit keep budget, + and keeps OpenClaw runtime's recent-tail cut point. Without an explicit keep budget, manual compaction remains a hard checkpoint and rebuilt context starts from the new summary. - Set `agents.defaults.compaction.midTurnPrecheck.enabled: true` to run the @@ -362,8 +362,8 @@ OpenClaw also enforces a safety floor for embedded runs: Why: leave enough headroom for multi-turn "housekeeping" (like memory writes) before compaction becomes unavoidable. -Implementation: `ensurePiCompactionReserveTokens()` in `src/agents/pi-settings.ts` -(called from `src/agents/pi-embedded-runner.ts`). +Implementation: `ensureAgentCompactionReserveTokens()` in `src/agents/agent-settings.ts` +(called from `src/agents/embedded-agent-runner.ts`). --- @@ -382,7 +382,7 @@ Plugins can register a compaction provider via `registerCompactionProvider()` on - If the provider fails or returns an empty result, OpenClaw falls back to built-in LLM summarization automatically. - Abort/timeout signals are re-thrown (not swallowed) to respect caller cancellation. -Source: `src/plugins/compaction-provider.ts`, `src/agents/pi-hooks/compaction-safeguard.ts`. +Source: `src/plugins/compaction-provider.ts`, `src/agents/agent-hooks/compaction-safeguard.ts`. --- @@ -427,7 +427,7 @@ erase critical context. OpenClaw uses the **pre-threshold flush** approach: 1. Monitor session context usage. -2. When it crosses a "soft threshold" (below Pi's compaction threshold), run a silent +2. When it crosses a "soft threshold" (below OpenClaw runtime's compaction threshold), run a silent "write memory now" directive to the agent. 3. Use the exact silent token `NO_REPLY` / `no_reply` so the user sees nothing. @@ -448,11 +448,11 @@ Notes: active session fallback chain, so local-only housekeeping does not silently fall back to a paid conversation model. - The flush runs once per compaction cycle (tracked in `sessions.json`). -- The flush runs only for embedded Pi sessions (CLI backends skip it). +- The flush runs only for embedded OpenClaw sessions (CLI backends skip it). - The flush is skipped when the session workspace is read-only (`workspaceAccess: "ro"` or `"none"`). - See [Memory](/concepts/memory) for the workspace file layout and write patterns. -Pi also exposes a `session_before_compact` hook in the extension API, but OpenClaw's +OpenClaw also exposes a `session_before_compact` hook in the extension API, but OpenClaw's flush logic lives on the Gateway side today. --- diff --git a/docs/reference/transcript-hygiene.md b/docs/reference/transcript-hygiene.md index bc1825fe658..465530b96c3 100644 --- a/docs/reference/transcript-hygiene.md +++ b/docs/reference/transcript-hygiene.md @@ -33,7 +33,7 @@ If you need transcript storage details, see: Runtime/system context can be added to the model prompt for a turn, but it is not end-user-authored content. OpenClaw keeps a separate transcript-facing -prompt body for Gateway replies, queued followups, ACP, CLI, and embedded Pi +prompt body for Gateway replies, queued followups, ACP, CLI, and embedded OpenClaw runs. Stored visible user turns use that transcript body instead of the runtime-enriched prompt. @@ -48,7 +48,7 @@ TUI, REST, or SSE clients. All transcript hygiene is centralized in the embedded runner: - Policy selection: `src/agents/transcript-policy.ts` -- Sanitization/repair application: `sanitizeSessionHistory` in `src/agents/pi-embedded-runner/replay-history.ts` +- Sanitization/repair application: `sanitizeSessionHistory` in `src/agents/embedded-agent-runner/replay-history.ts` The policy uses `provider`, `modelApi`, and `modelId` to decide what to apply. @@ -69,7 +69,7 @@ Lower max dimensions generally reduce token usage; higher dimensions preserve de Implementation: -- `sanitizeSessionMessagesImages` in `src/agents/pi-embedded-helpers/images.ts` +- `sanitizeSessionMessagesImages` in `src/agents/embedded-agent-helpers/images.ts` - `sanitizeContentBlocksImages` in `src/agents/tool-images.ts` - Max image side is configurable via `agents.defaults.imageMaxDimensionPx` (default: `1200`). - Blank text blocks are removed while this pass walks replay content. Assistant @@ -87,7 +87,7 @@ persisted tool calls (for example, after a rate limit failure). Implementation: - `sanitizeToolCallInputs` in `src/agents/session-transcript-repair.ts` -- Applied in `sanitizeSessionHistory` in `src/agents/pi-embedded-runner/replay-history.ts` +- Applied in `sanitizeSessionHistory` in `src/agents/embedded-agent-runner/replay-history.ts` --- diff --git a/docs/tools/acp-agents-setup.md b/docs/tools/acp-agents-setup.md index 9a7d2995c06..d59faa75b6c 100644 --- a/docs/tools/acp-agents-setup.md +++ b/docs/tools/acp-agents-setup.md @@ -41,7 +41,6 @@ Current acpx built-in harness aliases: - `kiro` - `openclaw` - `opencode` -- `pi` - `qwen` When OpenClaw uses the acpx backend, prefer these values for `agentId` unless your acpx config defines custom agent aliases. @@ -79,7 +78,7 @@ Core ACP baseline: "kiro", "openclaw", "opencode", - "pi", + "openclaw", "qwen", ], maxConcurrentSessions: 8, diff --git a/docs/tools/acp-agents.md b/docs/tools/acp-agents.md index a6da96113f2..bb046543d98 100644 --- a/docs/tools/acp-agents.md +++ b/docs/tools/acp-agents.md @@ -11,7 +11,7 @@ sidebarTitle: "ACP agents" --- [Agent Client Protocol (ACP)](https://agentclientprotocol.com/) sessions -let OpenClaw run external coding harnesses (for example Pi, Claude Code, +let OpenClaw run external coding harnesses (for example Claude Code, Cursor, Copilot, Droid, OpenClaw ACP, OpenCode, Gemini CLI, and other supported ACPX harnesses) through an ACP backend plugin. @@ -109,7 +109,6 @@ or `sessions_spawn({ runtime: "acp", agentId: "" })` targets: | `kiro` | Kiro CLI | Adapter availability and model control depend on the installed CLI. | | `opencode` | OpenCode ACP adapter | Requires OpenCode CLI/provider auth. | | `openclaw` | OpenClaw Gateway bridge through `openclaw acp` | Lets an ACP-aware harness talk back to an OpenClaw Gateway session. | -| `pi` | Pi/embedded OpenClaw runtime | Used for OpenClaw-native harness experiments. | | `qwen` | Qwen Code / Qwen CLI | Requires Qwen-compatible auth on the host. | Custom acpx agent aliases can be configured in acpx itself, but OpenClaw diff --git a/docs/tools/index.md b/docs/tools/index.md index 9751855d4a7..fc08df5ddec 100644 --- a/docs/tools/index.md +++ b/docs/tools/index.md @@ -29,7 +29,7 @@ only when the agent should see fewer tools or needs explicit host access. | Add a new integration or runtime surface | [Plugins](#extend-capabilities) | [Plugins](/tools/plugin) and [Build plugins](/plugins/building-plugins) | | Run work later or in the background | [Automation](/automation) | [Automation overview](/automation) | | Coordinate multiple agents or harnesses | [Sub-agents](/tools/subagents) | [ACP agents](/tools/acp-agents) and [Agent send](/tools/agent-send) | -| Search a large PI tool catalog | [Tool Search](/tools/tool-search) | [Tool Search](/tools/tool-search) | +| Search a large OpenClaw tool catalog | [Tool Search](/tools/tool-search) | [Tool Search](/tools/tool-search) | ## Choose tools, skills, or plugins @@ -78,21 +78,21 @@ The table lists representative tools so you can recognize the surface. It is not the full policy reference. For exact groups, defaults, and allow/deny semantics, use [Tools and custom providers](/gateway/config-tools). -| Category | Use when the agent needs to... | Representative tools | Read next | -| ---------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------- | -| Runtime | Run commands, manage processes, or use provider-backed Python analysis | `exec`, `process`, `code_execution` | [Exec](/tools/exec), [Code execution](/tools/code-execution) | -| Files | Read and change workspace files | `read`, `write`, `edit`, `apply_patch` | [Apply patch](/tools/apply-patch) | -| Web | Search the web, search X posts, or fetch readable page content | `web_search`, `x_search`, `web_fetch` | [Web tools](/tools/web), [Web fetch](/tools/web-fetch) | -| Browser | Operate a browser session | `browser` | [Browser](/tools/browser) | -| Messaging and channels | Send replies or channel actions | `message` | [Agent send](/tools/agent-send) | -| Sessions and agents | Inspect sessions, delegate work, steer another run, or report status | `sessions_*`, `subagents`, `agents_list`, `session_status` | [Sub-agents](/tools/subagents), [Session tool](/concepts/session-tool) | -| Automation | Schedule work or respond to background events | `cron`, `heartbeat_respond` | [Automation](/automation) | -| Gateway and nodes | Inspect Gateway state or paired target devices | `gateway`, `nodes` | [Gateway configuration](/gateway/configuration), [Nodes](/nodes) | -| Media | Analyze, generate, or speak media | `image`, `image_generate`, `music_generate`, `video_generate`, `tts` | [Media overview](/tools/media-overview) | -| Large PI catalogs | Search and call many eligible tools without sending every schema to the model | `tool_search_code`, `tool_search`, `tool_describe` | [Tool Search](/tools/tool-search) | +| Category | Use when the agent needs to... | Representative tools | Read next | +| ----------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| Runtime | Run commands, manage processes, or use provider-backed Python analysis | `exec`, `process`, `code_execution` | [Exec](/tools/exec), [Code execution](/tools/code-execution) | +| Files | Read and change workspace files | `read`, `write`, `edit`, `apply_patch` | [Apply patch](/tools/apply-patch) | +| Web | Search the web, search X posts, or fetch readable page content | `web_search`, `x_search`, `web_fetch` | [Web tools](/tools/web), [Web fetch](/tools/web-fetch) | +| Browser | Operate a browser session | `browser` | [Browser](/tools/browser) | +| Messaging and channels | Send replies or channel actions | `message` | [Agent send](/tools/agent-send) | +| Sessions and agents | Inspect sessions, delegate work, steer another run, or report status | `sessions_*`, `subagents`, `agents_list`, `session_status` | [Sub-agents](/tools/subagents), [Session tool](/concepts/session-tool) | +| Automation | Schedule work or respond to background events | `cron`, `heartbeat_respond` | [Automation](/automation) | +| Gateway and nodes | Inspect Gateway state or paired target devices | `gateway`, `nodes` | [Gateway configuration](/gateway/configuration), [Nodes](/nodes) | +| Media | Analyze, generate, or speak media | `image`, `image_generate`, `music_generate`, `video_generate`, `tts` | [Media overview](/tools/media-overview) | +| Large OpenClaw catalogs | Search and call many eligible tools without sending every schema to the model | `tool_search_code`, `tool_search`, `tool_describe` | [Tool Search](/tools/tool-search) | -Tool Search is an experimental PI-agent surface. Codex harness runs use +Tool Search is an experimental OpenClaw agent surface. Codex harness runs use Codex-native code mode, native tool search, deferred dynamic tools, and nested tool calls instead of `tools.toolSearch`. @@ -164,7 +164,7 @@ current turn: [Plugins](/tools/plugin). 5. For delegated runs, check per-agent restrictions in [Per-agent sandbox and tool restrictions](/tools/multi-agent-sandbox-tools). -6. For large PI catalogs, confirm whether the run uses direct tool exposure or +6. For large OpenClaw catalogs, confirm whether the run uses direct tool exposure or [Tool Search](/tools/tool-search). ## Related @@ -175,4 +175,4 @@ current turn: - [Plugins](/tools/plugin) for plugin installation and management - [Plugin SDK](/plugins/sdk-overview) for plugin author reference - [Skills](/tools/skills) for skill load order, gating, and config -- [Tool Search](/tools/tool-search) for compact PI tool catalog discovery +- [Tool Search](/tools/tool-search) for compact OpenClaw tool catalog discovery diff --git a/docs/tools/skills.md b/docs/tools/skills.md index 2624ccba0b9..eada364bca2 100644 --- a/docs/tools/skills.md +++ b/docs/tools/skills.md @@ -387,7 +387,7 @@ under `skills.entries` in `~/.openclaw/openclaw.json`: `false` disables the skill even if it is bundled or installed. The bundled `coding-agent` skill is opt-in: set `skills.entries.coding-agent.enabled: true` before exposing it to agents, - then make sure one of `claude`, `codex`, `opencode`, or `pi` is installed and + then make sure one of `claude`, `codex`, `opencode`, or another supported CLI is installed and authenticated for its own CLI. @@ -495,7 +495,7 @@ skills that cannot currently run there. When skills are eligible, OpenClaw injects a compact XML list of available skills into the system prompt (via `formatSkillsForPrompt` in -`pi-coding-agent`). The cost is deterministic: +`session runtime`). The cost is deterministic: - **Base overhead** (only when ≥1 skill): 195 characters. - **Per skill:** 97 characters + the length of the XML-escaped ``, ``, and `` values. diff --git a/docs/tools/slash-commands.md b/docs/tools/slash-commands.md index 0069265fd6e..2ea45ec70c1 100644 --- a/docs/tools/slash-commands.md +++ b/docs/tools/slash-commands.md @@ -312,7 +312,7 @@ For profile and override editing, use the Control UI Tools panel or config/catal - **Provider usage/quota** (example: "Claude 80% left") shows up in `/status` for the current model provider when usage tracking is enabled. OpenClaw normalizes provider windows to `% left`; for MiniMax, remaining-only percent fields are inverted before display, and `model_remains` responses prefer the chat-model entry plus a model-tagged plan label. - **Token/cache lines** in `/status` can fall back to the latest transcript usage entry when the live session snapshot is sparse. Existing nonzero live values still win, and transcript fallback can also recover the active runtime model label plus a larger prompt-oriented total when stored totals are missing or smaller. -- **Execution vs runtime:** `/status` reports `Execution` for the effective sandbox path and `Runtime` for who is actually running the session: `OpenClaw Pi Default`, `OpenAI Codex`, a CLI backend, or an ACP backend. +- **Execution vs runtime:** `/status` reports `Execution` for the effective sandbox path and `Runtime` for who is actually running the session: `OpenClaw Default`, `OpenAI Codex`, a CLI backend, or an ACP backend. - **Per-response tokens/cost** is controlled by `/usage off|tokens|full` (appended to normal replies). - `/model status` is about **models/auth/endpoints**, not usage. @@ -409,7 +409,7 @@ Examples: ``` -`/mcp` stores config in OpenClaw config, not Pi-owned project settings. Runtime adapters decide which transports are actually executable. +`/mcp` stores config in OpenClaw config, not embedded-agent project settings. Runtime adapters decide which transports are actually executable. ## Plugin updates diff --git a/docs/tools/subagents.md b/docs/tools/subagents.md index 81d35948e27..b79a6013805 100644 --- a/docs/tools/subagents.md +++ b/docs/tools/subagents.md @@ -354,7 +354,7 @@ that would run unsandboxed. Use `agents_list` to see which agent ids are currently allowed for `sessions_spawn`. The response includes each listed agent's effective -model and embedded runtime metadata so callers can distinguish PI, Codex +model and embedded runtime metadata so callers can distinguish OpenClaw, Codex app-server, and other configured native runtimes. `allowAgents` entries must point at configured agent ids in `agents.list[]`. diff --git a/docs/tools/thinking.md b/docs/tools/thinking.md index c237bdfc15c..24f41bb4007 100644 --- a/docs/tools/thinking.md +++ b/docs/tools/thinking.md @@ -55,7 +55,7 @@ title: "Thinking levels" ## Application by agent -- **Embedded Pi**: the resolved level is passed to the in-process Pi agent runtime. +- **Embedded OpenClaw**: the resolved level is passed to the in-process OpenClaw agent runtime. - **Claude CLI backend**: non-off levels are passed to Claude Code as `--effort` when using `claude-cli`; see [CLI backends](/gateway/cli-backends). ## Fast mode (/fast) @@ -83,7 +83,7 @@ title: "Thinking levels" - `/verbose off` stores an explicit session override; clear it via the Sessions UI by choosing `inherit`. - Inline directive affects only that message; session/global defaults apply otherwise. - Send `/verbose` (or `/verbose:`) with no argument to see the current verbose level. -- When verbose is on, agents that emit structured tool results (Pi, other JSON agents) send each tool call back as its own metadata-only message, prefixed with ` : ` when available. These tool summaries are sent as soon as each tool starts (separate bubbles), not as streaming deltas. +- When verbose is on, agents that emit structured tool results send each tool call back as its own metadata-only message, prefixed with ` : ` when available. These tool summaries are sent as soon as each tool starts (separate bubbles), not as streaming deltas. - Tool failure summaries remain visible in normal mode, but raw error detail suffixes are hidden unless verbose is `full`. - When verbose is `full`, tool outputs are also forwarded after completion (separate bubble, truncated to a safe length). If you toggle `/verbose on|full|off` while a run is in-flight, subsequent tool bubbles honor the new setting. - `agents.defaults.toolProgressDetail` controls the shape of `/verbose` tool summaries and progress-draft tool lines. Use `"explain"` (default) for compact human labels such as `🛠️ Exec: checking JS syntax`; use `"raw"` when you also want the raw command/detail appended for debugging. Per-agent `agents.list[].toolProgressDetail` overrides the default. diff --git a/docs/tools/tokenjuice.md b/docs/tools/tokenjuice.md index de7a76b4052..b44cb013fa9 100644 --- a/docs/tools/tokenjuice.md +++ b/docs/tools/tokenjuice.md @@ -13,7 +13,7 @@ tool results after the command has already run. It changes the returned `tool_result`, not the command itself. Tokenjuice does not rewrite shell input, rerun commands, or change exit codes. -Today this applies to PI embedded runs and OpenClaw dynamic tools in the Codex +Today this applies to OpenClaw embedded runs and OpenClaw dynamic tools in the Codex app-server harness. Tokenjuice hooks OpenClaw's tool-result middleware and trims the output before it goes back into the active harness session. diff --git a/docs/tools/tool-search.md b/docs/tools/tool-search.md index 20676d24618..6e69352295f 100644 --- a/docs/tools/tool-search.md +++ b/docs/tools/tool-search.md @@ -1,22 +1,22 @@ --- -summary: "Tool Search: compact large PI tool catalogs behind search, describe, and call" +summary: "Tool Search: compact large OpenClaw tool catalogs behind search, describe, and call" title: "Tool Search" read_when: - - You want PI agents to use a large tool catalog without adding every tool schema to the prompt - - You want OpenClaw tools, MCP tools, and client tools exposed through one compact PI surface - - You are implementing or debugging tool discovery for PI runs + - You want OpenClaw agents to use a large tool catalog without adding every tool schema to the prompt + - You want OpenClaw tools, MCP tools, and client tools exposed through one compact runtime surface + - You are implementing or debugging tool discovery for OpenClaw runs --- -Tool Search is an experimental OpenClaw PI-agent feature. It gives PI agents one +Tool Search is an experimental OpenClaw agent runtime feature. It gives agents one compact way to discover and call large tool catalogs. It is useful when the run has many available tools but the model is likely to need only a few of them. -This page documents OpenClaw PI Tool Search. It is not the Codex-native tool +This page documents OpenClaw Tool Search. It is not the Codex-native tool search or dynamic-tools surface. Codex-native code mode, tool search, deferred dynamic tools, and nested tool calls are stable Codex harness surfaces and do not depend on `tools.toolSearch`. -When enabled for PI, the model receives one `tool_search_code` tool by default. +When enabled for OpenClaw runs, the model receives one `tool_search_code` tool by default. That tool runs a short JavaScript body in an isolated Node subprocess with an `openclaw.tools` bridge: @@ -41,7 +41,7 @@ tools, and nested tool calls. ## How a turn runs -At planning time the PI embedded runner builds the effective catalog for the +At planning time the OpenClaw embedded runner builds the effective catalog for the run: 1. Resolve the active tool policy for the agent, profile, sandbox, and session. @@ -49,7 +49,7 @@ run: 3. List eligible MCP tools through the session MCP runtime. 4. Add eligible client tools supplied for the current run. 5. Index compact descriptors for search. -6. Expose either the PI code bridge or the structured fallback tools to the +6. Expose either the OpenClaw code bridge or the structured fallback tools to the model. At execution time every real tool call returns to OpenClaw. The isolated Node @@ -70,7 +70,7 @@ shape the model sees. If the current runtime cannot launch the isolated Node code-mode child process, the default `code` mode falls back to `tools` before catalog compaction. -Both modes are experimental. Prefer direct tool exposure for small PI tool +Both modes are experimental. Prefer direct tool exposure for small OpenClaw tool catalogs, and prefer the Codex-native stable surfaces for Codex harness runs. There is no separate source-selection config. When Tool Search is enabled, the @@ -158,7 +158,7 @@ Normal OpenClaw behavior still applies to final calls: ## Config -Enable Tool Search for PI runs with the default code bridge: +Enable Tool Search for OpenClaw runs with the default code bridge: ```bash openclaw config set tools.toolSearch true @@ -174,7 +174,7 @@ Equivalent JSON: } ``` -Use the structured fallback tools instead for PI runs: +Use the structured fallback tools instead for OpenClaw runs: ```json5 { @@ -230,7 +230,7 @@ Session logs should make it possible to answer: ## E2E validation -The gateway E2E runner proves both paths with the PI harness: +The gateway E2E runner proves both paths with the OpenClaw runtime: ```bash node --import tsx scripts/tool-search-gateway-e2e.ts diff --git a/docs/tools/trajectory.md b/docs/tools/trajectory.md index 53824c7a260..aa99c67016c 100644 --- a/docs/tools/trajectory.md +++ b/docs/tools/trajectory.md @@ -178,7 +178,7 @@ cleanup timeout is 10,000 ms. On slow disks or large stores, set export OPENCLAW_TRAJECTORY_FLUSH_TIMEOUT_MS=30000 ``` -This controls when OpenClaw logs a `pi-trajectory-flush` timeout and continues. +This controls when OpenClaw logs an `openclaw-trajectory-flush` timeout and continues. It does not change the trajectory size caps. To tune all agent cleanup steps that do not pass an explicit timeout, set `OPENCLAW_AGENT_CLEANUP_TIMEOUT_MS`. diff --git a/docs/web/webchat.md b/docs/web/webchat.md index d5f8c84cf99..3f779970def 100644 --- a/docs/web/webchat.md +++ b/docs/web/webchat.md @@ -49,13 +49,13 @@ Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket. WebChat has two separate data paths: -- The session JSONL file is the durable model/runtime transcript. For normal agent runs, Pi persists model-visible `user`, `assistant`, and `toolResult` messages through its session manager. WebChat does not write arbitrary delivery, status, or helper text into that transcript. +- The session JSONL file is the durable model/runtime transcript. For normal agent runs, the embedded OpenClaw runtime persists model-visible `user`, `assistant`, and `toolResult` messages through its session manager. WebChat does not write arbitrary delivery, status, or helper text into that transcript. - Gateway `ReplyPayload` events are the live delivery projection. They can be normalized for WebChat/channel display, block streaming, directive tags, media embedding, TTS/audio flags, and UI fallback behavior. They are not themselves the canonical session log. - Harnesses that require visible replies through `tools.message` still use WebChat as a current-run internal source reply sink. A targetless `message.send` from that active WebChat run is projected into the same chat and mirrored to the session transcript; WebChat does not become a reusable outbound channel and never inherits `lastChannel`. -- WebChat injects assistant transcript entries only when the Gateway owns a displayed message outside a normal Pi assistant turn: `chat.inject`, non-agent command replies, aborted partial output, and WebChat-managed media transcript supplements. +- WebChat injects assistant transcript entries only when the Gateway owns a displayed message outside a normal embedded agent turn: `chat.inject`, non-agent command replies, aborted partial output, and WebChat-managed media transcript supplements. - `chat.history` reads the stored session transcript and applies WebChat display projection. If live assistant text appears during a run but disappears after history reload, first check whether the raw JSONL contains the assistant text, then whether `chat.history` projection stripped it, then whether the Control UI optimistic-tail merge replaced local delivery state with the persisted snapshot. -Normal agent-run final answers should be durable because Pi writes the assistant `message_end`. Any fallback that mirrors a delivered final payload into the transcript must first avoid duplicating an assistant turn that Pi already wrote. +Normal agent-run final answers should be durable because the embedded runtime writes the assistant `message_end`. Any fallback that mirrors a delivered final payload into the transcript must first avoid duplicating an assistant turn that the embedded runtime already wrote. ## Control UI agents tools panel diff --git a/qa/scenarios/agents/instruction-followthrough-repo-contract.md b/qa/scenarios/agents/instruction-followthrough-repo-contract.md index 8a7d756d298..dc998f1aa5a 100644 --- a/qa/scenarios/agents/instruction-followthrough-repo-contract.md +++ b/qa/scenarios/agents/instruction-followthrough-repo-contract.md @@ -20,7 +20,7 @@ docsRefs: - docs/channels/qa-channel.md codeRefs: - src/agents/system-prompt.ts - - src/agents/pi-embedded-runner/run/incomplete-turn.ts + - src/agents/embedded-agent-runner/run/incomplete-turn.ts - extensions/qa-lab/src/mock-openai-server.ts execution: kind: flow diff --git a/qa/scenarios/index.md b/qa/scenarios/index.md index 7c246d742e8..3528440dfc3 100644 --- a/qa/scenarios/index.md +++ b/qa/scenarios/index.md @@ -27,13 +27,13 @@ Coverage tracking: Runtime parity tiers: -- `standard`: required Codex-vs-Pi mock gate coverage for first-hour depth and +- `standard`: required Codex-vs-OpenClaw mock gate coverage for first-hour depth and default runtime-tool fixtures. OpenClaw dynamic integration tools in this tier are hard-gated by `openclaw qa coverage --tools --summary`; Codex-native workspace rows remain separately tracked until native/live behavior is the asserted surface. Rows that explicitly target searchable/deferred OpenClaw dynamic loading stay report-only unless a fixture promotes them to required. Selected with - `openclaw qa suite --runtime-pair pi,codex --runtime-parity-tier standard` + `openclaw qa suite --runtime-pair openclaw,codex --runtime-parity-tier standard` - `optional`: profile-, plugin-, or external-service-dependent runtime-tool fixtures that stay out of the default release gate - `live-only`: scenarios that need real provider/runtime behavior rather than diff --git a/qa/scenarios/models/codex-harness-no-meta-leak.md b/qa/scenarios/models/codex-harness-no-meta-leak.md index ec521640b06..eade1026379 100644 --- a/qa/scenarios/models/codex-harness-no-meta-leak.md +++ b/qa/scenarios/models/codex-harness-no-meta-leak.md @@ -72,9 +72,8 @@ steps: patch: agents: defaults: - agentRuntime: - id: - expr: config.harnessRuntime + models: + expr: "({ [env.primaryModel]: { agentRuntime: { id: config.harnessRuntime } } })" - call: waitForGatewayHealthy args: - ref: env @@ -88,10 +87,10 @@ steps: args: - ref: env - assert: - expr: "snapshot.config.agents?.defaults?.agentRuntime?.id === config.harnessRuntime" + expr: "snapshot.config.agents?.defaults?.models?.[env.primaryModel]?.agentRuntime?.id === config.harnessRuntime" message: - expr: "`expected agentRuntime.id=${config.harnessRuntime}, got ${JSON.stringify(snapshot.config.agents?.defaults?.agentRuntime)}`" - detailsExpr: "env.providerMode === 'live-frontier' ? `provider=${selected?.provider} model=${selected?.model} runtime=${snapshot.config.agents?.defaults?.agentRuntime?.id}` : `mock mode: parsed ${scenario.id}`" + expr: "`expected ${env.primaryModel} agentRuntime.id=${config.harnessRuntime}, got ${JSON.stringify(snapshot.config.agents?.defaults?.models?.[env.primaryModel]?.agentRuntime)}`" + detailsExpr: "env.providerMode === 'live-frontier' ? `provider=${selected?.provider} model=${selected?.model} runtime=${snapshot.config.agents?.defaults?.models?.[env.primaryModel]?.agentRuntime?.id}` : `mock mode: parsed ${scenario.id}`" - name: keeps codex coordination chatter out of the visible reply actions: - if: diff --git a/qa/scenarios/models/gpt55-thinking-visibility-switch.md b/qa/scenarios/models/gpt55-thinking-visibility-switch.md index 5551fc65b1d..8158d3882d0 100644 --- a/qa/scenarios/models/gpt55-thinking-visibility-switch.md +++ b/qa/scenarios/models/gpt55-thinking-visibility-switch.md @@ -23,7 +23,7 @@ docsRefs: codeRefs: - src/auto-reply/reply/directives.ts - src/auto-reply/thinking.shared.ts - - src/agents/pi-embedded-runner/run/payloads.ts + - src/agents/embedded-agent-runner/run/payloads.ts - extensions/openai/openai-provider.ts - extensions/qa-lab/src/providers/mock-openai/server.ts execution: diff --git a/qa/scenarios/runtime/approval-turn-tool-followthrough.md b/qa/scenarios/runtime/approval-turn-tool-followthrough.md index 72b001e1dc8..f0d7c8bf6c7 100644 --- a/qa/scenarios/runtime/approval-turn-tool-followthrough.md +++ b/qa/scenarios/runtime/approval-turn-tool-followthrough.md @@ -20,7 +20,7 @@ docsRefs: codeRefs: - extensions/qa-lab/src/suite.ts - extensions/qa-lab/src/mock-openai-server.ts - - src/agents/pi-embedded-runner/run/incomplete-turn.ts + - src/agents/embedded-agent-runner/run/incomplete-turn.ts execution: kind: flow summary: Verify a short approval like "ok do it" triggers immediate tool use instead of fake-progress narration. diff --git a/qa/scenarios/runtime/auth-profile-doctor-migration-safety.md b/qa/scenarios/runtime/auth-profile-doctor-migration-safety.md index 78a2c56dbc9..c1760d93988 100644 --- a/qa/scenarios/runtime/auth-profile-doctor-migration-safety.md +++ b/qa/scenarios/runtime/auth-profile-doctor-migration-safety.md @@ -10,12 +10,12 @@ coverage: - runtime.doctor-repair secondary: - runtime.codex-plugin.auth -objective: Reproduce the four manual doctor-migration cells as an automated fixture matrix for Codex OAuth selection and stale Pi runtime pin removal. +objective: Reproduce the four manual doctor-migration cells as an automated fixture matrix for Codex OAuth selection and stale runtime pin removal. successCriteria: - OAuth-only hosts select the openai-codex OAuth profile and use the Codex harness. - Mixed-profile hosts still select openai-codex OAuth when an openai API-key profile exists. - - Mixed-profile defaults-level pi runtime pins are stripped by doctor repair. - - Mixed-profile per-agent pi runtime pins are stripped by doctor repair. + - Mixed-profile defaults-level legacy runtime pins are stripped by doctor repair. + - Mixed-profile per-agent legacy runtime pins are stripped by doctor repair. docsRefs: - docs/cli/doctor.md codeRefs: @@ -24,13 +24,13 @@ codeRefs: - extensions/qa-lab/src/codex-plugin-lifecycle.test.ts execution: kind: flow - summary: Exercise the four-cell doctor migration matrix against Codex auth and stale Pi runtime pins. + summary: Exercise the four-cell doctor migration matrix against Codex auth and stale runtime pins. config: matrixCells: - oauth-only - mixed-no-pin - - mixed-defaults-pi-pin - - mixed-main-agent-pi-pin + - mixed-defaults-legacy-pin + - mixed-main-agent-legacy-pin ``` ```yaml qa-flow @@ -56,7 +56,7 @@ steps: expr: "cell === 'oauth-only' ? 'oauth-only' : 'mixed'" - set: doctorConfig value: - expr: "cell === 'mixed-defaults-pi-pin' ? { agents: { defaults: { agentRuntime: { id: 'pi' } } } } : cell === 'mixed-main-agent-pi-pin' ? { agents: { list: { main: { agentRuntime: { id: 'pi' } } } } } : {}" + expr: "cell === 'mixed-defaults-legacy-pin' ? { agents: { defaults: { agentRuntime: { id: 'pi' } } } } : cell === 'mixed-main-agent-legacy-pin' ? { agents: { list: { main: { agentRuntime: { id: 'pi' } } } } } : {}" - try: actions: - call: plugin.seedCodexPluginAt @@ -77,7 +77,7 @@ steps: - assert: expr: "(Object.keys(doctorConfig).length === 0 && result.removedRuntimePins.length === 0) || result.removedRuntimePins.includes('agentRuntime.id=pi')" message: - expr: "`doctor matrix cell ${cell} did not report stale Pi pin cleanup: ${JSON.stringify(result)}`" + expr: "`doctor matrix cell ${cell} did not report stale runtime pin cleanup: ${JSON.stringify(result)}`" finally: - call: fs.rm args: diff --git a/qa/scenarios/runtime/codex-pi-shaped-read-vocabulary.md b/qa/scenarios/runtime/codex-legacy-read-tool-vocabulary.md similarity index 75% rename from qa/scenarios/runtime/codex-pi-shaped-read-vocabulary.md rename to qa/scenarios/runtime/codex-legacy-read-tool-vocabulary.md index c44a227bafc..55daa337b8c 100644 --- a/qa/scenarios/runtime/codex-pi-shaped-read-vocabulary.md +++ b/qa/scenarios/runtime/codex-legacy-read-tool-vocabulary.md @@ -1,8 +1,8 @@ -# Codex Pi-shaped Read vocabulary canary +# Codex legacy Read tool vocabulary canary ```yaml qa-scenario -id: codex-pi-shaped-read-vocabulary -title: Codex Pi-shaped Read vocabulary canary +id: codex-legacy-read-tool-vocabulary +title: Codex legacy Read tool vocabulary canary surface: runtime runtimeParityTier: live-only coverage: @@ -11,7 +11,7 @@ coverage: secondary: - runtime.prompt-compatibility - tools.fs.read -objective: Verify Codex-mode agents can satisfy legacy Pi-shaped "Read tool" wording through the native Codex workspace-read capability instead of stopping because duplicate OpenClaw dynamic read is intentionally filtered. +objective: Verify Codex-mode agents can satisfy legacy "Read tool" wording through the native Codex workspace-read capability instead of stopping because duplicate OpenClaw dynamic read is intentionally filtered. successCriteria: - Agent reads the seeded workspace file and replies with the exact marker line. - Agent does not claim that the Read tool is unavailable. @@ -24,11 +24,11 @@ codeRefs: - extensions/qa-lab/src/suite.ts execution: kind: flow - summary: Seed a workspace file, ask with Pi-shaped "Read tool" wording, and require Codex to complete the read through its native workspace capability. + summary: Seed a workspace file, ask with legacy "Read tool" wording, and require Codex to complete the read through its native workspace capability. config: runtimeParityComparison: codex-native-workspace - fixtureFile: PI_SHAPED_READ_FIXTURE.txt - expectedMarker: PI_SHAPED_READ_OK + fixtureFile: LEGACY_READ_TOOL_FIXTURE.txt + expectedMarker: LEGACY_READ_TOOL_OK unavailableNeedles: - not in my available tool surface - read tool is not @@ -41,7 +41,7 @@ execution: ```yaml qa-flow steps: - - name: handles Pi-shaped Read tool wording with native Codex read + - name: handles legacy Read tool wording with native Codex read actions: - call: waitForGatewayHealthy args: @@ -67,7 +67,7 @@ steps: args: - ref: env - sessionKey: - expr: "`agent:qa:pi-shaped-read:${randomUUID().slice(0, 8)}`" + expr: "`agent:qa:legacy-read:${randomUUID().slice(0, 8)}`" message: expr: "`Use the Read tool to read ${fixturePath}. Reply with the exact marker line and nothing else.`" timeoutMs: @@ -94,10 +94,10 @@ steps: - assert: expr: "normalizedOutbound.includes(normalizeLowercaseStringOrEmpty(config.expectedMarker))" message: - expr: "`Pi-shaped Read vocabulary canary did not read marker ${config.expectedMarker}; outbound=${outboundText}`" + expr: "`legacy Read vocabulary canary did not read marker ${config.expectedMarker}; outbound=${outboundText}`" - assert: expr: "!unavailableNeedles.some((needle) => normalizedOutbound.includes(needle))" message: - expr: "`Pi-shaped Read vocabulary canary stopped on unavailable Read-tool wording: ${outboundText}`" + expr: "`legacy Read vocabulary canary stopped on unavailable Read-tool wording: ${outboundText}`" detailsExpr: outbound.text ``` diff --git a/qa/scenarios/runtime/compaction-retry-mutating-tool.md b/qa/scenarios/runtime/compaction-retry-mutating-tool.md index 31702d4f67e..1f690e3f30a 100644 --- a/qa/scenarios/runtime/compaction-retry-mutating-tool.md +++ b/qa/scenarios/runtime/compaction-retry-mutating-tool.md @@ -21,8 +21,8 @@ docsRefs: codeRefs: - extensions/qa-lab/src/suite.ts - extensions/qa-lab/src/mock-openai-server.ts - - src/agents/pi-embedded-subscribe.ts - - src/agents/pi-embedded-subscribe.handlers.lifecycle.ts + - src/agents/embedded-agent-subscribe.ts + - src/agents/embedded-agent-subscribe.handlers.lifecycle.ts execution: kind: flow summary: Verify a mutating tool step keeps replay-unsafety explicit through compaction or retry pressure. diff --git a/qa/scenarios/runtime/empty-response-recovery-replay-safe-read.md b/qa/scenarios/runtime/empty-response-recovery-replay-safe-read.md index d84107c0e42..aed629ebf93 100644 --- a/qa/scenarios/runtime/empty-response-recovery-replay-safe-read.md +++ b/qa/scenarios/runtime/empty-response-recovery-replay-safe-read.md @@ -19,7 +19,7 @@ docsRefs: - docs/help/testing.md codeRefs: - extensions/qa-lab/src/mock-openai-server.ts - - src/agents/pi-embedded-runner/run/incomplete-turn.ts + - src/agents/embedded-agent-runner/run/incomplete-turn.ts execution: kind: flow summary: Verify empty OpenAI turns recover after a replay-safe read. diff --git a/qa/scenarios/runtime/empty-response-retry-budget-exhausted.md b/qa/scenarios/runtime/empty-response-retry-budget-exhausted.md index 51fa187ca83..6840cdbaf25 100644 --- a/qa/scenarios/runtime/empty-response-retry-budget-exhausted.md +++ b/qa/scenarios/runtime/empty-response-retry-budget-exhausted.md @@ -18,7 +18,7 @@ docsRefs: - docs/help/testing.md codeRefs: - extensions/qa-lab/src/mock-openai-server.ts - - src/agents/pi-embedded-runner/run/incomplete-turn.ts + - src/agents/embedded-agent-runner/run/incomplete-turn.ts execution: kind: flow summary: Verify empty-response retry exhaustion still surfaces a visible failure. diff --git a/qa/scenarios/runtime/reasoning-only-no-auto-retry-after-write.md b/qa/scenarios/runtime/reasoning-only-no-auto-retry-after-write.md index 026c55bf796..770a50073b2 100644 --- a/qa/scenarios/runtime/reasoning-only-no-auto-retry-after-write.md +++ b/qa/scenarios/runtime/reasoning-only-no-auto-retry-after-write.md @@ -20,7 +20,7 @@ docsRefs: - docs/help/gpt55-codex-agentic-parity.md codeRefs: - extensions/qa-lab/src/mock-openai-server.ts - - src/agents/pi-embedded-runner/run/incomplete-turn.ts + - src/agents/embedded-agent-runner/run/incomplete-turn.ts execution: kind: flow summary: Verify reasoning-only turns after a write do not auto-retry. diff --git a/qa/scenarios/runtime/reasoning-only-recovery-replay-safe-read.md b/qa/scenarios/runtime/reasoning-only-recovery-replay-safe-read.md index 1696cc6cadb..f64d110f7e5 100644 --- a/qa/scenarios/runtime/reasoning-only-recovery-replay-safe-read.md +++ b/qa/scenarios/runtime/reasoning-only-recovery-replay-safe-read.md @@ -19,7 +19,7 @@ docsRefs: - docs/help/testing.md codeRefs: - extensions/qa-lab/src/mock-openai-server.ts - - src/agents/pi-embedded-runner/run/incomplete-turn.ts + - src/agents/embedded-agent-runner/run/incomplete-turn.ts execution: kind: flow summary: Verify reasoning-only OpenAI turns recover after a replay-safe read. diff --git a/qa/scenarios/runtime/streaming-final-integrity.md b/qa/scenarios/runtime/streaming-final-integrity.md index cfb24776159..57a66326737 100644 --- a/qa/scenarios/runtime/streaming-final-integrity.md +++ b/qa/scenarios/runtime/streaming-final-integrity.md @@ -20,7 +20,7 @@ docsRefs: - docs/concepts/streaming.md - docs/channels/qa-channel.md codeRefs: - - src/agents/pi-embedded-runner/run/incomplete-turn.ts + - src/agents/embedded-agent-runner/run/incomplete-turn.ts - extensions/qa-lab/src/bus-state.ts - extensions/qa-lab/src/suite-runtime-transport.ts execution: diff --git a/qa/scenarios/runtime/tools/apply-patch.md b/qa/scenarios/runtime/tools/apply-patch.md index 3ce8c3fd5f3..1b21a81a097 100644 --- a/qa/scenarios/runtime/tools/apply-patch.md +++ b/qa/scenarios/runtime/tools/apply-patch.md @@ -8,9 +8,9 @@ runtimeParityTier: standard coverage: primary: - tools.apply-patch -objective: Verify apply_patch behavior is tracked across Pi and Codex while Codex owns patching natively. +objective: Verify apply_patch behavior is tracked across OpenClaw and Codex while Codex owns patching natively. successCriteria: - - Pi may expose OpenClaw apply_patch while Codex app-server mode may omit duplicate OpenClaw dynamic apply_patch. + - OpenClaw may expose OpenClaw apply_patch while Codex app-server mode may omit duplicate OpenClaw dynamic apply_patch. - Mock provider apply_patch plans are reported as fixture intent, not as actual runtime tool calls. - The row stays report-only until fault injection uses valid patch-shaped inputs. docsRefs: diff --git a/qa/scenarios/runtime/tools/bash.md b/qa/scenarios/runtime/tools/bash.md index 4dbccd0bd46..887e0d21e15 100644 --- a/qa/scenarios/runtime/tools/bash.md +++ b/qa/scenarios/runtime/tools/bash.md @@ -8,15 +8,15 @@ runtimeParityTier: standard coverage: primary: - tools.bash -objective: Verify shell command behavior is tracked across Pi and Codex while Codex owns exec/process natively. +objective: Verify shell command behavior is tracked across OpenClaw and Codex while Codex owns exec/process natively. successCriteria: - - Pi may expose OpenClaw exec while Codex app-server mode may omit duplicate OpenClaw dynamic exec/process. + - OpenClaw may expose OpenClaw exec while Codex app-server mode may omit duplicate OpenClaw dynamic exec/process. - Mock provider exec plans are reported as fixture intent, not as actual runtime tool calls. - The row stays report-only until the fixture validates native Codex command behavior directly. docsRefs: - qa/scenarios/index.md codeRefs: - - src/agents/pi-tools.ts + - src/agents/agent-tools.ts - src/agents/bash-tools.schemas.ts - extensions/qa-lab/src/runtime-tool-fixture.ts execution: diff --git a/qa/scenarios/runtime/tools/edit.md b/qa/scenarios/runtime/tools/edit.md index bdecb0ed852..caaa5900cb9 100644 --- a/qa/scenarios/runtime/tools/edit.md +++ b/qa/scenarios/runtime/tools/edit.md @@ -8,15 +8,15 @@ runtimeParityTier: standard coverage: primary: - tools.edit -objective: Verify targeted edit behavior is tracked across Pi and Codex while Codex owns edit natively. +objective: Verify targeted edit behavior is tracked across OpenClaw and Codex while Codex owns edit natively. successCriteria: - - Pi may expose OpenClaw edit while Codex app-server mode may omit duplicate OpenClaw dynamic edit. + - OpenClaw may expose OpenClaw edit while Codex app-server mode may omit duplicate OpenClaw dynamic edit. - Mock provider edit plans are reported as fixture intent, not as actual runtime tool calls. - The row stays report-only until the fixture validates native Codex edit behavior directly. docsRefs: - qa/scenarios/index.md codeRefs: - - src/agents/pi-tools.ts + - src/agents/agent-tools.ts - extensions/qa-lab/src/runtime-tool-fixture.ts execution: kind: flow diff --git a/qa/scenarios/runtime/tools/exec.md b/qa/scenarios/runtime/tools/exec.md index 1248ca4b544..c7e0d557393 100644 --- a/qa/scenarios/runtime/tools/exec.md +++ b/qa/scenarios/runtime/tools/exec.md @@ -8,9 +8,9 @@ runtimeParityTier: standard coverage: primary: - tools.exec -objective: Verify command execution behavior is tracked across Pi and Codex while Codex owns exec/process natively. +objective: Verify command execution behavior is tracked across OpenClaw and Codex while Codex owns exec/process natively. successCriteria: - - Pi may expose OpenClaw exec while Codex app-server mode may omit duplicate OpenClaw dynamic exec/process. + - OpenClaw may expose OpenClaw exec while Codex app-server mode may omit duplicate OpenClaw dynamic exec/process. - Mock provider exec plans are reported as fixture intent, not as actual runtime tool calls. - The row stays report-only until the fixture validates native Codex command behavior directly. docsRefs: diff --git a/qa/scenarios/runtime/tools/fs-list.md b/qa/scenarios/runtime/tools/fs-list.md index 6827ecadf3b..ce8629841fc 100644 --- a/qa/scenarios/runtime/tools/fs-list.md +++ b/qa/scenarios/runtime/tools/fs-list.md @@ -10,13 +10,13 @@ coverage: - tools.fs.list objective: Verify directory inspection behavior is tracked through read while Codex owns file inspection natively. successCriteria: - - Pi may expose OpenClaw read while Codex app-server mode may omit duplicate OpenClaw dynamic read. + - OpenClaw may expose OpenClaw read while Codex app-server mode may omit duplicate OpenClaw dynamic read. - Mock provider read plans are reported as fixture intent, not as actual runtime tool calls. - The row stays report-only until directory fault injection proves native Codex read behavior directly. docsRefs: - qa/scenarios/index.md codeRefs: - - src/agents/pi-tools.read.ts + - src/agents/agent-tools.read.ts - extensions/qa-lab/src/runtime-tool-fixture.ts execution: kind: flow diff --git a/qa/scenarios/runtime/tools/fs-read.md b/qa/scenarios/runtime/tools/fs-read.md index 24d2ca03c1a..d1055b6b099 100644 --- a/qa/scenarios/runtime/tools/fs-read.md +++ b/qa/scenarios/runtime/tools/fs-read.md @@ -8,15 +8,15 @@ runtimeParityTier: standard coverage: primary: - tools.fs.read -objective: Verify file read behavior is tracked across Pi and Codex while Codex owns read natively. +objective: Verify file read behavior is tracked across OpenClaw and Codex while Codex owns read natively. successCriteria: - - Pi may expose OpenClaw read while Codex app-server mode may omit duplicate OpenClaw dynamic read. + - OpenClaw may expose OpenClaw read while Codex app-server mode may omit duplicate OpenClaw dynamic read. - Mock provider read plans are reported as fixture intent, not as actual runtime tool calls. - The row stays report-only until failure-path injection proves native Codex read behavior directly. docsRefs: - qa/scenarios/index.md codeRefs: - - src/agents/pi-tools.read.ts + - src/agents/agent-tools.read.ts - extensions/qa-lab/src/runtime-tool-fixture.ts execution: kind: flow diff --git a/qa/scenarios/runtime/tools/fs-write.md b/qa/scenarios/runtime/tools/fs-write.md index e53d0d410c8..654ce7cce42 100644 --- a/qa/scenarios/runtime/tools/fs-write.md +++ b/qa/scenarios/runtime/tools/fs-write.md @@ -8,15 +8,15 @@ runtimeParityTier: standard coverage: primary: - tools.fs.write -objective: Verify file write behavior is tracked across Pi and Codex while Codex owns write natively. +objective: Verify file write behavior is tracked across OpenClaw and Codex while Codex owns write natively. successCriteria: - - Pi may expose OpenClaw write while Codex app-server mode may omit duplicate OpenClaw dynamic write. + - OpenClaw may expose OpenClaw write while Codex app-server mode may omit duplicate OpenClaw dynamic write. - Mock provider write plans are reported as fixture intent, not as actual runtime tool calls. - The row stays report-only until the fixture validates native Codex write behavior directly. docsRefs: - qa/scenarios/index.md codeRefs: - - src/agents/pi-tools.workspace-paths.test.ts + - src/agents/agent-tools.workspace-paths.test.ts - extensions/qa-lab/src/runtime-tool-fixture.ts execution: kind: flow diff --git a/qa/scenarios/runtime/tools/grep.md b/qa/scenarios/runtime/tools/grep.md index 0e51c448b7d..556976369ba 100644 --- a/qa/scenarios/runtime/tools/grep.md +++ b/qa/scenarios/runtime/tools/grep.md @@ -10,13 +10,13 @@ coverage: - tools.grep objective: Verify grep-style search behavior is tracked through command execution while Codex owns exec/process natively. successCriteria: - - Pi may expose OpenClaw exec while Codex app-server mode may omit duplicate OpenClaw dynamic exec/process. + - OpenClaw may expose OpenClaw exec while Codex app-server mode may omit duplicate OpenClaw dynamic exec/process. - Mock provider exec plans are reported as fixture intent, not as actual runtime tool calls. - The row stays report-only until the fixture validates native Codex search/command behavior directly. docsRefs: - qa/scenarios/index.md codeRefs: - - src/agents/pi-tools.ts + - src/agents/agent-tools.ts - extensions/qa-lab/src/runtime-tool-fixture.ts execution: kind: flow diff --git a/qa/scenarios/runtime/tools/image-generate.md b/qa/scenarios/runtime/tools/image-generate.md index cce41c035a0..19ccc3f162d 100644 --- a/qa/scenarios/runtime/tools/image-generate.md +++ b/qa/scenarios/runtime/tools/image-generate.md @@ -8,7 +8,7 @@ runtimeParityTier: standard coverage: primary: - tools.image-generate -objective: Verify image_generate preserves arguments and result shape across Pi and Codex. +objective: Verify image_generate preserves arguments and result shape across OpenClaw and Codex. successCriteria: - Effective tools expose image_generate after QA image-generation config is applied. - The mock provider plans exactly one happy-path image_generate call. @@ -35,7 +35,7 @@ execution: codexDefaultImpact: P4 qaImpact: P1 action: hard gate in the standard direct-loading tier - reason: image_generate is an OpenClaw integration tool and must stay visible and callable under Pi and Codex direct runtime parity. + reason: image_generate is an OpenClaw integration tool and must stay visible and callable under OpenClaw and Codex direct runtime parity. promptSnippet: "target=image_generate" failurePromptSnippet: "failure target=image_generate" ``` diff --git a/qa/scenarios/runtime/tools/message-tool.md b/qa/scenarios/runtime/tools/message-tool.md index 306f63e8306..b84fc37922d 100644 --- a/qa/scenarios/runtime/tools/message-tool.md +++ b/qa/scenarios/runtime/tools/message-tool.md @@ -16,7 +16,7 @@ successCriteria: docsRefs: - qa/scenarios/index.md codeRefs: - - src/agents/pi-embedded-messaging.ts + - src/agents/embedded-agent-messaging.ts - src/agents/tools/sessions-send-tool.ts - extensions/qa-lab/src/runtime-tool-fixture.ts execution: diff --git a/qa/scenarios/runtime/tools/session-status.md b/qa/scenarios/runtime/tools/session-status.md index 1bc27644244..d292212b994 100644 --- a/qa/scenarios/runtime/tools/session-status.md +++ b/qa/scenarios/runtime/tools/session-status.md @@ -8,7 +8,7 @@ runtimeParityTier: standard coverage: primary: - tools.session-status -objective: Verify session_status preserves arguments and result shape across Pi and Codex. +objective: Verify session_status preserves arguments and result shape across OpenClaw and Codex. successCriteria: - Effective tools expose session_status. - The mock provider plans exactly one happy-path session_status call. @@ -34,7 +34,7 @@ execution: codexDefaultImpact: P4 qaImpact: P1 action: hard gate in the standard direct-loading tier - reason: session_status is an OpenClaw integration tool and must stay visible and callable under Pi and Codex direct runtime parity. + reason: session_status is an OpenClaw integration tool and must stay visible and callable under OpenClaw and Codex direct runtime parity. promptSnippet: "target=session_status" failurePromptSnippet: "failure target=session_status" ``` diff --git a/qa/scenarios/runtime/tools/sessions-spawn.md b/qa/scenarios/runtime/tools/sessions-spawn.md index ae05fbd0b07..971506dcd0e 100644 --- a/qa/scenarios/runtime/tools/sessions-spawn.md +++ b/qa/scenarios/runtime/tools/sessions-spawn.md @@ -8,7 +8,7 @@ runtimeParityTier: standard coverage: primary: - tools.sessions-spawn -objective: Verify sessions_spawn preserves arguments and result shape across Pi and Codex. +objective: Verify sessions_spawn preserves arguments and result shape across OpenClaw and Codex. successCriteria: - Effective tools expose sessions_spawn. - The mock provider plans exactly one happy-path sessions_spawn call. @@ -34,7 +34,7 @@ execution: codexDefaultImpact: P4 qaImpact: P1 action: hard gate in the standard direct-loading tier - reason: sessions_spawn is an OpenClaw integration tool and must stay visible and callable under Pi and Codex direct runtime parity. + reason: sessions_spawn is an OpenClaw integration tool and must stay visible and callable under OpenClaw and Codex direct runtime parity. promptSnippet: "target=sessions_spawn" failurePromptSnippet: "failure target=sessions_spawn" ``` diff --git a/qa/scenarios/runtime/tools/web-fetch.md b/qa/scenarios/runtime/tools/web-fetch.md index 5ed2e353984..47d0c0a1fc4 100644 --- a/qa/scenarios/runtime/tools/web-fetch.md +++ b/qa/scenarios/runtime/tools/web-fetch.md @@ -8,7 +8,7 @@ runtimeParityTier: standard coverage: primary: - tools.web-fetch -objective: Verify web_fetch preserves arguments and result shape across Pi and Codex. +objective: Verify web_fetch preserves arguments and result shape across OpenClaw and Codex. successCriteria: - Effective tools expose web_fetch. - The mock provider plans exactly one happy-path web_fetch call. @@ -34,7 +34,7 @@ execution: codexDefaultImpact: P4 qaImpact: P1 action: hard gate in the standard direct-loading tier - reason: web_fetch is an OpenClaw integration tool and must stay visible and callable under Pi and Codex direct runtime parity. + reason: web_fetch is an OpenClaw integration tool and must stay visible and callable under OpenClaw and Codex direct runtime parity. promptSnippet: "target=web_fetch" failurePromptSnippet: "failure target=web_fetch" ``` diff --git a/qa/scenarios/runtime/tools/web-search.md b/qa/scenarios/runtime/tools/web-search.md index 5712ada13c4..2c2a5ffeea5 100644 --- a/qa/scenarios/runtime/tools/web-search.md +++ b/qa/scenarios/runtime/tools/web-search.md @@ -8,7 +8,7 @@ runtimeParityTier: standard coverage: primary: - tools.web-search -objective: Verify web_search preserves arguments and result shape across Pi and Codex. +objective: Verify web_search preserves arguments and result shape across OpenClaw and Codex. successCriteria: - Effective tools expose web_search. - The mock provider plans exactly one happy-path web_search call. @@ -34,7 +34,7 @@ execution: codexDefaultImpact: P4 qaImpact: P1 action: hard gate in the standard direct-loading tier - reason: web_search is an OpenClaw integration tool and must stay visible and callable under Pi and Codex direct runtime parity. + reason: web_search is an OpenClaw integration tool and must stay visible and callable under OpenClaw and Codex direct runtime parity. promptSnippet: "target=web_search" failurePromptSnippet: "failure target=web_search" ``` diff --git a/qa/scenarios/security/secret-redaction-tool-logs.md b/qa/scenarios/security/secret-redaction-tool-logs.md index f85f6ebef1d..d90a26641c2 100644 --- a/qa/scenarios/security/secret-redaction-tool-logs.md +++ b/qa/scenarios/security/secret-redaction-tool-logs.md @@ -21,7 +21,7 @@ docsRefs: codeRefs: - extensions/qa-lab/src/suite-runtime-agent-process.ts - extensions/qa-lab/src/suite-runtime-transport.ts - - src/agents/pi-embedded-runner/run/incomplete-turn.ts + - src/agents/embedded-agent-runner/run/incomplete-turn.ts execution: kind: flow summary: Verify fake secret fixtures are not echoed into channel-visible output. diff --git a/qa/scenarios/workspace/medium-game-plan-codex-harness.md b/qa/scenarios/workspace/medium-game-plan-codex-harness.md index ae2b3207e3f..4158640ee09 100644 --- a/qa/scenarios/workspace/medium-game-plan-codex-harness.md +++ b/qa/scenarios/workspace/medium-game-plan-codex-harness.md @@ -77,9 +77,8 @@ steps: patch: agents: defaults: - agentRuntime: - id: - expr: config.harnessRuntime + models: + expr: "({ [env.primaryModel]: { agentRuntime: { id: config.harnessRuntime } } })" - call: waitForGatewayHealthy args: - ref: env @@ -93,10 +92,10 @@ steps: args: - ref: env - assert: - expr: "snapshot.config.agents?.defaults?.agentRuntime?.id === config.harnessRuntime" + expr: "snapshot.config.agents?.defaults?.models?.[env.primaryModel]?.agentRuntime?.id === config.harnessRuntime" message: - expr: "`expected agentRuntime.id=${config.harnessRuntime}, got ${JSON.stringify(snapshot.config.agents?.defaults?.agentRuntime)}`" - detailsExpr: "env.providerMode === 'live-frontier' ? `provider=${selected?.provider} model=${selected?.model} runtime=${snapshot.config.agents?.defaults?.agentRuntime?.id}` : `mock mode: parsed ${scenario.id}`" + expr: "`expected ${env.primaryModel} agentRuntime.id=${config.harnessRuntime}, got ${JSON.stringify(snapshot.config.agents?.defaults?.models?.[env.primaryModel]?.agentRuntime)}`" + detailsExpr: "env.providerMode === 'live-frontier' ? `provider=${selected?.provider} model=${selected?.model} runtime=${snapshot.config.agents?.defaults?.models?.[env.primaryModel]?.agentRuntime?.id}` : `mock mode: parsed ${scenario.id}`" - name: builds the medium game artifact actions: - if: diff --git a/qa/scenarios/workspace/medium-game-plan-pi-harness.md b/qa/scenarios/workspace/medium-game-plan-openclaw-harness.md similarity index 82% rename from qa/scenarios/workspace/medium-game-plan-pi-harness.md rename to qa/scenarios/workspace/medium-game-plan-openclaw-harness.md index c38862ea5be..4d41f40fd80 100644 --- a/qa/scenarios/workspace/medium-game-plan-pi-harness.md +++ b/qa/scenarios/workspace/medium-game-plan-openclaw-harness.md @@ -1,18 +1,18 @@ -# Medium game plan PI harness +# Medium game plan OpenClaw harness ```yaml qa-scenario -id: medium-game-plan-pi-harness -title: Medium game plan PI harness +id: medium-game-plan-openclaw-harness +title: Medium game plan OpenClaw harness surface: workspace coverage: primary: - workspace.planning secondary: - - agents.pi-harness -objective: Verify GPT-5.5 can use the PI harness to plan and build a medium-complex self-contained browser game. + - agents.openclaw-harness +objective: Verify GPT-5.5 can use the OpenClaw harness to plan and build a medium-complex self-contained browser game. successCriteria: - A live-frontier run fails fast unless the selected primary model is openai/gpt-5.5. - - The scenario forces the embedded PI harness before the build turn. + - The scenario forces the embedded OpenClaw harness before the build turn. - The prompt explicitly asks the agent to enter plan mode before editing. - The agent writes a self-contained HTML game with a canvas loop, controls, scoring, waves, pause, and restart. docsRefs: @@ -21,22 +21,22 @@ docsRefs: - docs/help/testing.md codeRefs: - src/agents/harness/selection.ts - - src/agents/harness/builtin-pi.ts + - src/agents/harness/builtin-openclaw.ts - extensions/qa-lab/src/suite.ts execution: kind: flow - summary: Run with `pnpm openclaw qa suite --provider-mode live-frontier --model openai/gpt-5.5 --alt-model openai/gpt-5.5 --fast --thinking medium --scenario medium-game-plan-pi-harness`. + summary: Run with `pnpm openclaw qa suite --provider-mode live-frontier --model openai/gpt-5.5 --alt-model openai/gpt-5.5 --fast --thinking medium --scenario medium-game-plan-openclaw-harness`. config: requiredProvider: openai requiredModel: gpt-5.5 - harnessRuntime: pi - artifactFile: star-garden-defenders-pi.html + harnessRuntime: openclaw + artifactFile: star-garden-defenders-openclaw.html gameTitle: Star Garden Defenders minBytes: 5000 buildPrompt: |- Enter plan mode first and write a short implementation plan before editing. - Then build a medium-complex, self-contained browser game at ./star-garden-defenders-pi.html. + Then build a medium-complex, self-contained browser game at ./star-garden-defenders-openclaw.html. Game: Star Garden Defenders. Requirements: @@ -51,7 +51,7 @@ execution: ```yaml qa-flow steps: - - name: confirms GPT-5.5 PI harness target + - name: confirms GPT-5.5 OpenClaw harness target actions: - set: selected value: @@ -77,9 +77,8 @@ steps: patch: agents: defaults: - agentRuntime: - id: - expr: config.harnessRuntime + models: + expr: "({ [env.primaryModel]: { agentRuntime: { id: config.harnessRuntime } } })" - call: waitForGatewayHealthy args: - ref: env @@ -93,10 +92,10 @@ steps: args: - ref: env - assert: - expr: "snapshot.config.agents?.defaults?.agentRuntime?.id === config.harnessRuntime" + expr: "snapshot.config.agents?.defaults?.models?.[env.primaryModel]?.agentRuntime?.id === config.harnessRuntime" message: - expr: "`expected agentRuntime.id=${config.harnessRuntime}, got ${JSON.stringify(snapshot.config.agents?.defaults?.agentRuntime)}`" - detailsExpr: "env.providerMode === 'live-frontier' ? `provider=${selected?.provider} model=${selected?.model} runtime=${snapshot.config.agents?.defaults?.agentRuntime?.id}` : `mock mode: parsed ${scenario.id}`" + expr: "`expected ${env.primaryModel} agentRuntime.id=${config.harnessRuntime}, got ${JSON.stringify(snapshot.config.agents?.defaults?.models?.[env.primaryModel]?.agentRuntime)}`" + detailsExpr: "env.providerMode === 'live-frontier' ? `provider=${selected?.provider} model=${selected?.model} runtime=${snapshot.config.agents?.defaults?.models?.[env.primaryModel]?.agentRuntime?.id}` : `mock mode: parsed ${scenario.id}`" - name: builds the medium game artifact actions: - if: @@ -108,7 +107,7 @@ steps: - call: runAgentPrompt args: - ref: env - - sessionKey: agent:qa:medium-game-pi + - sessionKey: agent:qa:medium-game-openclaw message: expr: config.buildPrompt provider: diff --git a/scripts/audit-seams.mjs b/scripts/audit-seams.mjs index c1147f26d66..310492e9723 100644 --- a/scripts/audit-seams.mjs +++ b/scripts/audit-seams.mjs @@ -584,7 +584,7 @@ function describeCronSeamKinds(relativePath, source) { const seamKinds = []; const importsAgentRunner = hasAnyImportSource(source, [ "../../agents/cli-runner.js", - "../../agents/pi-embedded.js", + "../../agents/embedded-agent.js", "../../agents/model-fallback.js", "../../agents/subagent-registry.js", "../../infra/agent-events.js", @@ -625,7 +625,7 @@ function describeCronSeamKinds(relativePath, source) { if ( importsAgentRunner && - /\brunCliAgent\b|\brunEmbeddedPiAgent\b|\brunWithModelFallback\b|\bregisterAgentRunContext\b/.test( + /\brunCliAgent\b|\brunEmbeddedAgent\b|\brunWithModelFallback\b|\bregisterAgentRunContext\b/.test( source, ) ) { @@ -746,7 +746,7 @@ function describeSubagentSeamKinds(relativePath, source) { if ( (importsAnnounceDelivery || isAnnounceDispatchPath) && - /\brunSubagentAnnounceFlow\b|\brunSubagentAnnounceDispatch\b|\benqueueAnnounce\b|\bcreateBoundDeliveryRouter\b|\bqueueEmbeddedPiMessage\b|\bwaitForEmbeddedPiRunEnd\b|\bqueue-fallback\b|\bdirect-primary\b/.test( + /\brunSubagentAnnounceFlow\b|\brunSubagentAnnounceDispatch\b|\benqueueAnnounce\b|\bcreateBoundDeliveryRouter\b|\bqueueEmbeddedAgentMessage\b|\bwaitForEmbeddedAgentRunEnd\b|\bqueue-fallback\b|\bdirect-primary\b/.test( source, ) ) { diff --git a/scripts/bench-model.ts b/scripts/bench-model.ts index 707c6f87e7f..ddf8a527345 100644 --- a/scripts/bench-model.ts +++ b/scripts/bench-model.ts @@ -1,4 +1,4 @@ -import { completeSimple, getModel, type Api, type Model } from "@earendil-works/pi-ai"; +import { completeSimple, getModel, type Api, type Model } from "openclaw/plugin-sdk/llm"; type Usage = { input?: number; @@ -49,7 +49,7 @@ function median(values: number[]): number { async function runModel(opts: { label: string; - model: Model; + model: Model; apiKey: string; runs: number; prompt: string; diff --git a/scripts/build-all.mjs b/scripts/build-all.mjs index b3b18639dac..d354b9fcbfe 100644 --- a/scripts/build-all.mjs +++ b/scripts/build-all.mjs @@ -82,7 +82,7 @@ export const BUILD_ALL_STEPS = [ "scripts/lib/copy-assets.ts", "src/auto-reply/reply/export-html", ], - outputs: ["dist/export-html"], + outputs: ["dist/auto-reply/reply/export-html"], }, }, { diff --git a/scripts/control-ui-i18n.ts b/scripts/control-ui-i18n.ts index b03524ebedf..014aa3d39b9 100644 --- a/scripts/control-ui-i18n.ts +++ b/scripts/control-ui-i18n.ts @@ -1,16 +1,20 @@ -import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; import { existsSync } from "node:fs"; import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises"; -import { homedir } from "node:os"; import path from "node:path"; -import { createInterface } from "node:readline"; import { fileURLToPath, pathToFileURL } from "node:url"; import * as ts from "typescript"; import { formatErrorMessage } from "../src/infra/errors.ts"; -import { resolveNpmRunner } from "./npm-runner.mjs"; -import { resolvePnpmRunner } from "./pnpm-runner.mjs"; -import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs"; +import { + completeSimple, + getModel, + getProviders, + type Api, + type AssistantMessage, + type KnownProvider, + type Model, +} from "../src/llm/index.ts"; interface TranslationMap { [key: string]: string | TranslationMap; @@ -88,8 +92,6 @@ const CONTROL_UI_I18N_WORKFLOW = 1; const DEFAULT_OPENAI_MODEL = "gpt-5.5"; const DEFAULT_ANTHROPIC_MODEL = "claude-opus-4-6"; const DEFAULT_PROVIDER = "openai"; -export const DEFAULT_PI_PACKAGE_VERSION = "0.75.5"; -const PI_PACKAGE_NAME = "@earendil-works/pi-coding-agent"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(HERE, ".."); const LOCALES_DIR = path.join(ROOT, "ui", "src", "i18n", "locales"); @@ -108,9 +110,6 @@ const PROGRESS_HEARTBEAT_MS = 30_000; const ENV_PROVIDER = "OPENCLAW_CONTROL_UI_I18N_PROVIDER"; const ENV_MODEL = "OPENCLAW_CONTROL_UI_I18N_MODEL"; const ENV_THINKING = "OPENCLAW_CONTROL_UI_I18N_THINKING"; -const ENV_PI_EXECUTABLE = "OPENCLAW_CONTROL_UI_I18N_PI_EXECUTABLE"; -const ENV_PI_ARGS = "OPENCLAW_CONTROL_UI_I18N_PI_ARGS"; -const ENV_PI_PACKAGE_VERSION = "OPENCLAW_CONTROL_UI_I18N_PI_PACKAGE_VERSION"; const ENV_BATCH_CHAR_BUDGET = "OPENCLAW_CONTROL_UI_I18N_BATCH_CHAR_BUDGET"; const ENV_PROMPT_TIMEOUT = "OPENCLAW_CONTROL_UI_I18N_PROMPT_TIMEOUT"; const ENV_AUTH_OPTIONAL = "OPENCLAW_CONTROL_UI_I18N_AUTH_OPTIONAL"; @@ -273,6 +272,14 @@ function hasTranslationProvider(): boolean { return Boolean(process.env.OPENAI_API_KEY?.trim() || process.env.ANTHROPIC_API_KEY?.trim()); } +function resolveKnownTranslationProvider(): KnownProvider { + const provider = resolveConfiguredProvider(); + if (getProviders().includes(provider as KnownProvider)) { + return provider as KnownProvider; + } + throw new Error(`Unknown built-in provider: ${provider}`); +} + function normalizeText(text: string): string { return text.trim().split(/\s+/).join(" "); } @@ -288,7 +295,7 @@ function hashText(text: string): string { function cacheNamespace(): string { return [ `wf=${CONTROL_UI_I18N_WORKFLOW}`, - "engine=pi", + "engine=openclaw-llm", `provider=${resolveConfiguredProvider()}`, `model=${resolveConfiguredModel()}`, ].join("|"); @@ -918,236 +925,22 @@ function estimateBatchChars(items: readonly TranslationBatchItem[]): number { return items.reduce((total, item) => total + item.key.length + item.text.length + 8, 2); } -type PiCommand = { - args: string[]; - executable: string; -}; - -type ProcessCommand = { - args: string[]; - env?: NodeJS.ProcessEnv; - executable: string; - shell: boolean; - windowsVerbatimArguments?: boolean; -}; - -type ResolveProcessCommandOptions = { - comSpec?: string; - env?: NodeJS.ProcessEnv; - execPath?: string; - existsSync?: (path: string) => boolean; - npmExecPath?: string; - platform?: NodeJS.Platform; -}; - -function portableExtension(value: string): string { - return path.posix.extname(value.split(/[/\\]/u).at(-1) ?? value).toLowerCase(); -} - -function isWindowsCommandShim(value: string, platform = process.platform): boolean { - const extension = portableExtension(value); - return platform === "win32" && (extension === ".cmd" || extension === ".bat"); -} - -function resolveEnvValue(env: NodeJS.ProcessEnv, name: string): string | undefined { - const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase()); - return key === undefined ? undefined : env[key]; -} - -function commandFromRunner(runner: { - args: string[]; - command: string; - env?: NodeJS.ProcessEnv; - shell: boolean; - windowsVerbatimArguments?: boolean; -}): ProcessCommand { - const command: ProcessCommand = { - args: runner.args, - executable: runner.command, - shell: runner.shell, - windowsVerbatimArguments: runner.windowsVerbatimArguments, - }; - if (runner.env !== undefined) { - command.env = runner.env; - } - return command; -} - -export function resolveControlUiI18nProcessCommand( - executable: string, - args: string[], - options: ResolveProcessCommandOptions = {}, -): ProcessCommand { - const env = options.env ?? process.env; - const platform = options.platform ?? process.platform; - const comSpec = options.comSpec ?? resolveEnvValue(env, "ComSpec") ?? "cmd.exe"; - if (isWindowsCommandShim(executable, platform)) { - return { - args: ["/d", "/s", "/c", buildCmdExeCommandLine(executable, args)], - executable: comSpec, - shell: false, - windowsVerbatimArguments: true, - }; - } - return { args, executable, shell: false }; -} - -export function resolveControlUiI18nNpmInstallCommand( - packageSpec: string, - options: ResolveProcessCommandOptions = {}, -): ProcessCommand { - return commandFromRunner( - resolveNpmRunner({ - comSpec: options.comSpec, - env: options.env, - execPath: options.execPath, - existsSync: options.existsSync, - npmArgs: ["install", "--silent", "--no-audit", "--no-fund", packageSpec], - platform: options.platform, - }), - ); -} - -export function resolveControlUiI18nPnpmCommand( - args: string[], - options: ResolveProcessCommandOptions = {}, -): ProcessCommand { - return commandFromRunner( - resolvePnpmRunner({ - comSpec: options.comSpec, - npmExecPath: options.npmExecPath ?? process.env.npm_execpath, - nodeExecPath: options.execPath ?? process.execPath, - platform: options.platform, - pnpmArgs: args, - }), - ); -} - -export function resolvePiShimNodeCommand( - shimPath: string, - options: Pick = {}, -): PiCommand | null { - const platform = options.platform ?? process.platform; - if (!isWindowsCommandShim(shimPath, platform)) { - return null; - } - const cliPath = path.win32.join( - path.win32.dirname(shimPath), - "node_modules", - ...PI_PACKAGE_NAME.split("/"), - "dist", - "cli.js", - ); - const exists = options.existsSync ?? existsSync; - if (!exists(cliPath)) { - return null; - } - return { executable: "node", args: [cliPath] }; -} - -function resolvePiPackageVersion(): string { - return process.env[ENV_PI_PACKAGE_VERSION]?.trim() || DEFAULT_PI_PACKAGE_VERSION; -} - -function getPiRuntimeDir() { - return path.join( - homedir(), - ".cache", - "openclaw", - "control-ui-i18n", - "pi-runtime", - resolvePiPackageVersion(), - ); -} - -export function resolveLocalPiCommand(root = ROOT): PiCommand | null { - const cliPath = path.join(root, "node_modules", ...PI_PACKAGE_NAME.split("/"), "dist", "cli.js"); - if (!existsSync(cliPath)) { - return null; - } - return { executable: "node", args: [cliPath] }; -} - -async function resolvePiCommand(): Promise { - const explicitExecutable = process.env[ENV_PI_EXECUTABLE]?.trim(); - if (explicitExecutable) { - const explicitArgs = process.env[ENV_PI_ARGS]?.trim().split(/\s+/).filter(Boolean) ?? []; - const shimCommand = resolvePiShimNodeCommand(explicitExecutable); - if (shimCommand) { - return { - executable: shimCommand.executable, - args: [...shimCommand.args, ...explicitArgs], - }; - } - if (isWindowsCommandShim(explicitExecutable)) { - throw new Error( - `${ENV_PI_EXECUTABLE} points to a Windows command shim that cannot safely carry the multiline i18n system prompt. Point it at node with ${ENV_PI_ARGS} set to the Pi package dist/cli.js path, or unset it so OpenClaw uses the managed Pi runtime.`, - ); - } - return { - executable: explicitExecutable, - args: explicitArgs, - }; - } - - const pathEntries = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean); - for (const entry of pathEntries) { - const candidate = path.join(entry, process.platform === "win32" ? "pi.cmd" : "pi"); - if (existsSync(candidate)) { - const shimCommand = resolvePiShimNodeCommand(candidate); - if (shimCommand) { - return shimCommand; - } - if (process.platform === "win32") { - continue; - } - return { executable: candidate, args: [] }; - } - } - - const localCommand = resolveLocalPiCommand(); - if (localCommand) { - return localCommand; - } - - const runtimeDir = getPiRuntimeDir(); - const cliPath = path.join( - runtimeDir, - "node_modules", - ...PI_PACKAGE_NAME.split("/"), - "dist", - "cli.js", - ); - if (!existsSync(cliPath)) { - await mkdir(runtimeDir, { recursive: true }); - await runProcessCommand( - resolveControlUiI18nNpmInstallCommand(`${PI_PACKAGE_NAME}@${resolvePiPackageVersion()}`), - { - cwd: runtimeDir, - rejectOnFailure: true, - }, - ); - } - return { executable: "node", args: [cliPath] }; -} - type RunProcessOptions = { cwd?: string; input?: string; rejectOnFailure?: boolean; }; -async function runProcessCommand( - command: ProcessCommand, +async function runProcess( + executable: string, + args: string[], options: RunProcessOptions = {}, ): Promise<{ code: number; stderr: string; stdout: string }> { return await new Promise((resolve, reject) => { - const child = spawn(command.executable, command.args, { + const child = spawn(executable, args, { cwd: options.cwd ?? ROOT, - env: command.env ?? process.env, - shell: command.shell, + env: process.env, stdio: ["pipe", "pipe", "pipe"], - windowsVerbatimArguments: command.windowsVerbatimArguments, }); let stdout = ""; @@ -1167,11 +960,7 @@ async function runProcessCommand( child.once("close", (code) => { if ((code ?? 1) !== 0 && options.rejectOnFailure) { reject( - new Error( - `${command.executable} ${command.args.join(" ")} failed: ${ - stderr.trim() || stdout.trim() - }`, - ), + new Error(`${executable} ${args.join(" ")} failed: ${stderr.trim() || stdout.trim()}`), ); return; } @@ -1181,13 +970,9 @@ async function runProcessCommand( } async function formatGeneratedTypeScript(filePath: string, source: string): Promise { - const result = await runProcessCommand( - resolveControlUiI18nPnpmCommand([ - "exec", - "oxfmt", - "--stdin-filepath", - path.relative(ROOT, filePath), - ]), + const result = await runProcess( + "pnpm", + ["exec", "oxfmt", "--stdin-filepath", path.relative(ROOT, filePath)], { input: source, rejectOnFailure: true, @@ -1224,13 +1009,6 @@ function restoreReplacementCorruptedStringLiterals(source: string, formatted: st return `${output}${formatted.slice(cursor)}`; } -type PendingPrompt = { - id: string; - reject: (reason?: unknown) => void; - resolve: (value: string) => void; - responseReceived: boolean; -}; - type LocaleRunContext = { localeCount: number; localeIndex: number; @@ -1245,7 +1023,7 @@ type TranslationBatchContext = LocaleRunContext & { }; type ClientAccess = { - getClient: () => Promise; + getClient: () => Promise; resetClient: () => Promise; }; @@ -1284,198 +1062,74 @@ function buildTranslationBatches(items: readonly TranslationBatchItem[]): Transl return batches; } -class PiRpcClient { - private readonly stderrChunks: string[] = []; +export function resolveTranslationModel(): Model { + const provider = resolveKnownTranslationProvider(); + const modelId = resolveConfiguredModel(); + const model = getModel(provider, modelId as never); + if (!model) { + throw new Error(`Unknown built-in model: ${provider}/${modelId}`); + } + return model as Model; +} + +class TranslationClient { private closed = false; - private pending: PendingPrompt | null = null; - private readonly process: ChildProcessWithoutNullStreams; - private readonly stdin: ChildProcessWithoutNullStreams["stdin"]; - private requestCount = 0; private sequence: Promise = Promise.resolve(); + private readonly model: Model; - private constructor(processHandle: ChildProcessWithoutNullStreams) { - this.process = processHandle; - this.stdin = processHandle.stdin; + private constructor(private readonly systemPrompt: string) { + this.model = resolveTranslationModel(); } - static async create(systemPrompt: string): Promise { - const command = await resolvePiCommand(); - const args = [ - ...command.args, - "--mode", - "rpc", - "--provider", - resolveConfiguredProvider(), - "--model", - resolveConfiguredModel(), - "--thinking", - resolveThinkingLevel(), - "--no-session", - "--system-prompt", - systemPrompt, - ]; - const invocation = resolveControlUiI18nProcessCommand(command.executable, args); - const child = spawn(invocation.executable, invocation.args, { - cwd: ROOT, - env: invocation.env ?? process.env, - shell: invocation.shell, - stdio: ["pipe", "pipe", "pipe"], - windowsVerbatimArguments: invocation.windowsVerbatimArguments, - }); - - const client = new PiRpcClient(child); - client.bindProcess(); - await client.waitForBoot(); - return client; - } - - private bindProcess() { - const stderr = createInterface({ input: this.process.stderr }); - stderr.on("line", (line) => { - this.stderrChunks.push(line); - }); - - const stdout = createInterface({ input: this.process.stdout }); - stdout.on("line", (line) => { - void this.handleStdoutLine(line); - }); - - this.process.once("error", (error) => { - this.rejectPending(error); - }); - - this.process.once("close", () => { - this.closed = true; - this.rejectPending( - new Error(`pi process closed${this.stderr() ? ` (${this.stderr()})` : ""}`), - ); - }); - } - - private async waitForBoot() { - await sleep(150); - } - - private stderr() { - return this.stderrChunks.join("\n").trim(); - } - - private rejectPending(error: Error) { - const pending = this.pending; - this.pending = null; - if (pending) { - pending.reject(error); - } - } - - private async handleStdoutLine(line: string) { - const trimmed = line.trim(); - if (!trimmed) { - return; - } - let parsed: Record; - try { - parsed = JSON.parse(trimmed) as Record; - } catch { - return; - } - - const pending = this.pending; - if (!pending) { - return; - } - - switch (parsed.type) { - case "response": { - if (parsed.id !== pending.id) { - return; - } - const success = parsed.success === true; - if (!success) { - const errorText = - typeof parsed.error === "string" && parsed.error.trim() - ? parsed.error.trim() - : "pi prompt failed"; - this.pending = null; - pending.reject(new Error(errorText)); - return; - } - pending.responseReceived = true; - return; - } - case "agent_end": { - try { - const result = extractTranslationResult(parsed); - this.pending = null; - pending.resolve(result); - } catch (error) { - this.pending = null; - pending.reject(error); - } - } - } + static async create(systemPrompt: string): Promise { + return new TranslationClient(systemPrompt); } async prompt(message: string, label: string): Promise { const result = this.sequence.then(async () => { if (this.closed) { - throw new Error(`pi process unavailable${this.stderr() ? ` (${this.stderr()})` : ""}`); + throw new Error("translation runtime unavailable"); } - const id = `req-${++this.requestCount}`; - const payload = JSON.stringify({ type: "prompt", id, message }); const timeoutMs = resolvePromptTimeoutMs(); const startedAt = Date.now(); + const controller = new AbortController(); return await new Promise((resolve, reject) => { const heartbeat = setInterval(() => { - const responseState = this.pending?.responseReceived - ? "response=received" - : "response=pending"; logProgress( - `${label}: still waiting (${formatDuration(Date.now() - startedAt)} / ${formatDuration(timeoutMs)}, ${responseState})`, + `${label}: still waiting (${formatDuration(Date.now() - startedAt)} / ${formatDuration(timeoutMs)})`, ); }, PROGRESS_HEARTBEAT_MS); const timer = setTimeout(() => { - if (this.pending?.id === id) { - this.pending = null; - clearInterval(heartbeat); - void this.close(); - const stderr = this.stderr(); - reject( - new Error( - `${label}: translation prompt timed out after ${timeoutMs}ms${stderr ? ` (pi stderr: ${stderr})` : ""}`, - ), - ); - } + clearInterval(heartbeat); + controller.abort(); + reject(new Error(`${label}: translation prompt timed out after ${timeoutMs}ms`)); }, timeoutMs); - this.pending = { - id, - reject: (reason) => { + completeSimple( + this.model, + { + systemPrompt: this.systemPrompt, + messages: [{ role: "user", content: message, timestamp: Date.now() }], + }, + { + maxTokens: 4096, + reasoning: resolveThinkingLevel(), + signal: controller.signal, + timeoutMs, + }, + ) + .then((assistantMessage) => { clearTimeout(timer); clearInterval(heartbeat); - reject(reason); - }, - resolve: (value) => { + resolve(extractTranslationResult(assistantMessage)); + }) + .catch((error) => { clearTimeout(timer); clearInterval(heartbeat); - resolve(value); - }, - responseReceived: false, - }; - - this.stdin.write(`${payload}\n`, (error) => { - if (!error) { - return; - } - clearTimeout(timer); - clearInterval(heartbeat); - if (this.pending?.id === id) { - this.pending = null; - } - reject(error); - }); + reject(error); + }); }); }); @@ -1488,44 +1142,21 @@ class PiRpcClient { return; } this.closed = true; - this.stdin.end(); - this.process.kill("SIGTERM"); - await sleep(150); - if (!this.process.killed) { - this.process.kill("SIGKILL"); - } } } -function extractTranslationResult(payload: Record): string { - const messages = Array.isArray(payload.messages) ? payload.messages : []; - for (let index = messages.length - 1; index >= 0; index -= 1) { - const message = messages[index]; - if (!message || typeof message !== "object") { - continue; - } - if ((message as { role?: string }).role !== "assistant") { - continue; - } - const errorMessage = (message as { errorMessage?: string }).errorMessage; - const stopReason = (message as { stopReason?: string }).stopReason; - if (errorMessage || stopReason === "error") { - throw new Error(errorMessage?.trim() || "pi error"); - } - const content = (message as { content?: unknown }).content; - if (typeof content === "string") { - return content; - } - if (Array.isArray(content)) { - return content - .filter((block): block is { type?: string; text?: string } => - Boolean(block && typeof block === "object"), - ) - .map((block) => (block.type === "text" && typeof block.text === "string" ? block.text : "")) - .join(""); - } +function extractTranslationResult(message: AssistantMessage): string { + if (message.errorMessage || message.stopReason === "error") { + throw new Error(message.errorMessage?.trim() || "translation provider error"); } - throw new Error("assistant translation not found"); + const text = message.content + .map((block) => (block.type === "text" ? block.text : "")) + .join("") + .trim(); + if (!text) { + throw new Error("assistant translation not found"); + } + return text; } async function translateBatch( @@ -1674,11 +1305,11 @@ async function syncLocale( logProgress( `${localeLabel}: start keys=${sourceFlat.size} pending=${pending.length} batches=${batchCount} provider=${resolveConfiguredProvider()} model=${resolveConfiguredModel()} thinking=${resolveThinkingLevel()} timeout=${formatDuration(resolvePromptTimeoutMs())} batch_chars=${resolveBatchCharBudget()}`, ); - let client: PiRpcClient | null = null; + let client: TranslationClient | null = null; const clientAccess: ClientAccess = { async getClient() { if (!client) { - client = await PiRpcClient.create(buildSystemPrompt(entry.locale, glossary)); + client = await TranslationClient.create(buildSystemPrompt(entry.locale, glossary)); } return client; }, diff --git a/scripts/copy-export-html-templates.ts b/scripts/copy-export-html-templates.ts index ba815a3b125..a1e4cda544b 100644 --- a/scripts/copy-export-html-templates.ts +++ b/scripts/copy-export-html-templates.ts @@ -9,44 +9,51 @@ import { ensureDirectory, logVerboseCopy, resolveBuildCopyContext } from "./lib/ const context = resolveBuildCopyContext(import.meta.url); -const srcDir = path.join(context.projectRoot, "src", "auto-reply", "reply", "export-html"); -const distDir = path.join(context.projectRoot, "dist", "export-html"); +const exportHtmlSrcDir = path.join( + context.projectRoot, + "src", + "auto-reply", + "reply", + "export-html", +); +const exportHtmlDistDir = path.join( + context.projectRoot, + "dist", + "auto-reply", + "reply", + "export-html", +); function copyExportHtmlTemplates() { - if (!fs.existsSync(srcDir)) { - console.warn(`${context.prefix} Source directory not found:`, srcDir); + if (!fs.existsSync(exportHtmlSrcDir)) { + console.warn(`${context.prefix} Source directory not found:`, exportHtmlSrcDir); return; } - ensureDirectory(distDir); - - const templateFiles = ["template.html", "template.css", "template.js"]; + fs.rmSync(exportHtmlDistDir, { recursive: true, force: true }); + ensureDirectory(exportHtmlDistDir); let copiedCount = 0; - for (const file of templateFiles) { - const srcFile = path.join(srcDir, file); - const distFile = path.join(distDir, file); - if (fs.existsSync(srcFile)) { + + const copyDir = (srcDir: string, distDir: string, relativePrefix = "") => { + ensureDirectory(distDir); + for (const file of fs.readdirSync(srcDir)) { + const srcFile = path.join(srcDir, file); + const distFile = path.join(distDir, file); + const relativeName = path.join(relativePrefix, file); + if (file.endsWith(".test.ts")) { + continue; + } + if (fs.statSync(srcFile).isDirectory()) { + copyDir(srcFile, distFile, relativeName); + continue; + } fs.copyFileSync(srcFile, distFile); copiedCount += 1; - logVerboseCopy(context, `Copied ${file}`); + logVerboseCopy(context, `Copied ${relativeName}`); } - } + }; - const srcVendor = path.join(srcDir, "vendor"); - const distVendor = path.join(distDir, "vendor"); - if (fs.existsSync(srcVendor)) { - ensureDirectory(distVendor); - const vendorFiles = fs.readdirSync(srcVendor); - for (const file of vendorFiles) { - const srcFile = path.join(srcVendor, file); - const distFile = path.join(distVendor, file); - if (fs.statSync(srcFile).isFile()) { - fs.copyFileSync(srcFile, distFile); - copiedCount += 1; - logVerboseCopy(context, `Copied vendor/${file}`); - } - } - } + copyDir(exportHtmlSrcDir, exportHtmlDistDir); console.log(`${context.prefix} Copied ${copiedCount} export-html assets.`); } diff --git a/scripts/dev/channel-message-flows.ts b/scripts/dev/channel-message-flows.ts index 2bfc2ff6d6b..1bd9de30afd 100644 --- a/scripts/dev/channel-message-flows.ts +++ b/scripts/dev/channel-message-flows.ts @@ -19,7 +19,7 @@ import { createNativeTelegramToolProgressDraft, type NativeTelegramToolProgressDraft, } from "../../extensions/telegram/src/native-tool-progress-draft.js"; -import { formatReasoningMessage } from "../../src/agents/pi-embedded-utils.js"; +import { formatReasoningMessage } from "../../src/agents/embedded-agent-utils.js"; import { getRuntimeConfig } from "../../src/config/config.js"; import type { OpenClawConfig } from "../../src/config/types.openclaw.js"; import { formatChannelProgressDraftText } from "../../src/plugin-sdk/channel-outbound.js"; diff --git a/scripts/docker/install-sh-e2e/run.sh b/scripts/docker/install-sh-e2e/run.sh index ac09a3b1b03..828ac17c0b7 100755 --- a/scripts/docker/install-sh-e2e/run.sh +++ b/scripts/docker/install-sh-e2e/run.sh @@ -651,7 +651,7 @@ run_profile() { "$OPENAI_AGENT_MODEL" \ "openai/gpt-5.5" \ "openai/gpt-5.4-mini")" - openclaw --profile "$profile" config set models.providers.openai "{\"baseUrl\":\"https://api.openai.com/v1\",\"models\":[],\"timeoutSeconds\":${OPENAI_PROVIDER_TIMEOUT_SECONDS},\"agentRuntime\":{\"id\":\"pi\"}}" --strict-json >/dev/null + openclaw --profile "$profile" config set models.providers.openai "{\"baseUrl\":\"https://api.openai.com/v1\",\"models\":[],\"timeoutSeconds\":${OPENAI_PROVIDER_TIMEOUT_SECONDS},\"agentRuntime\":{\"id\":\"openclaw\"}}" --strict-json >/dev/null image_model="$(set_image_model "$profile" \ "openai/gpt-5.4-image-2")" else diff --git a/scripts/e2e/pi-bundle-mcp-tools-docker-client.ts b/scripts/e2e/agent-bundle-mcp-tools-docker-client.ts similarity index 86% rename from scripts/e2e/pi-bundle-mcp-tools-docker-client.ts rename to scripts/e2e/agent-bundle-mcp-tools-docker-client.ts index dce1f96cd95..26aac1b84f3 100644 --- a/scripts/e2e/pi-bundle-mcp-tools-docker-client.ts +++ b/scripts/e2e/agent-bundle-mcp-tools-docker-client.ts @@ -1,4 +1,4 @@ -// Pi bundle MCP tools Docker harness. +// OpenClaw bundle MCP tools Docker harness. // Imports packaged dist modules so tool materialization is verified against the // npm tarball installed in the functional image. import { randomUUID } from "node:crypto"; @@ -6,13 +6,13 @@ import fs from "node:fs/promises"; import { createRequire } from "node:module"; import os from "node:os"; import path from "node:path"; -import { materializeBundleMcpToolsForRun } from "../../dist/agents/pi-bundle-mcp-materialize.js"; +import { materializeBundleMcpToolsForRun } from "../../dist/agents/agent-bundle-mcp-materialize.js"; import { disposeAllSessionMcpRuntimes, getOrCreateSessionMcpRuntime, -} from "../../dist/agents/pi-bundle-mcp-runtime.js"; -import { applyFinalEffectiveToolPolicy } from "../../dist/agents/pi-embedded-runner/effective-tool-policy.js"; -import { splitSdkTools } from "../../dist/agents/pi-embedded-runner/tool-split.js"; +} from "../../dist/agents/agent-bundle-mcp-runtime.js"; +import { applyFinalEffectiveToolPolicy } from "../../dist/agents/embedded-agent-runner/effective-tool-policy.js"; +import { splitSdkTools } from "../../dist/agents/embedded-agent-runner/tool-split.js"; import type { OpenClawConfig } from "../../dist/config/types.openclaw.js"; import { getPluginToolMeta } from "../../dist/plugins/tools.js"; @@ -33,9 +33,9 @@ async function writeProbeServer(serverPath: string) { import { McpServer } from ${JSON.stringify(sdkMcpServerPath)}; import { StdioServerTransport } from ${JSON.stringify(sdkStdioServerPath)}; -const server = new McpServer({ name: "pi-bundle-mcp-tools-probe", version: "1.0.0" }); -server.tool("docker_probe", "Docker Pi MCP tool availability probe", async () => ({ - content: [{ type: "text", text: "pi-bundle-mcp-tools-ok" }], +const server = new McpServer({ name: "agent-bundle-mcp-tools-probe", version: "1.0.0" }); +server.tool("docker_probe", "Docker OpenClaw MCP tool availability probe", async () => ({ + content: [{ type: "text", text: "agent-bundle-mcp-tools-ok" }], })); await server.connect(new StdioServerTransport()); @@ -53,7 +53,7 @@ function applyPolicy(params: { tools: applyFinalEffectiveToolPolicy({ bundledTools: params.tools, config: params.config, - sessionKey: "agent:main:docker-pi-bundle-mcp", + sessionKey: "agent:main:docker-agent-bundle-mcp", agentId: "main", senderIsOwner: true, warn: (message) => { @@ -67,8 +67,8 @@ function applyPolicy(params: { async function main() { const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || - path.join(os.tmpdir(), `openclaw-pi-bundle-mcp-${process.pid}`); - const probeDir = path.join(stateDir, "pi-bundle-mcp-tools"); + path.join(os.tmpdir(), `openclaw-agent-bundle-mcp-${process.pid}`); + const probeDir = path.join(stateDir, "agent-bundle-mcp-tools"); const serverPath = path.join(probeDir, "probe-server.mjs"); await fs.mkdir(probeDir, { recursive: true }); await writeProbeServer(serverPath); @@ -91,8 +91,8 @@ async function main() { try { const runtime = await getOrCreateSessionMcpRuntime({ - sessionId: `docker-pi-bundle-mcp-${randomUUID()}`, - sessionKey: "agent:main:docker-pi-bundle-mcp", + sessionId: `docker-agent-bundle-mcp-${randomUUID()}`, + sessionKey: "agent:main:docker-agent-bundle-mcp", workspaceDir: probeDir, cfg, }); @@ -106,7 +106,9 @@ async function main() { const result = await probeTool.execute("docker-mcp-probe", {}, undefined, undefined); assert( - result.content.some((item) => item.type === "text" && item.text === "pi-bundle-mcp-tools-ok"), + result.content.some( + (item) => item.type === "text" && item.text === "agent-bundle-mcp-tools-ok", + ), "expected materialized MCP tool execution result", ); diff --git a/scripts/e2e/pi-bundle-mcp-tools-docker.sh b/scripts/e2e/agent-bundle-mcp-tools-docker.sh similarity index 61% rename from scripts/e2e/pi-bundle-mcp-tools-docker.sh rename to scripts/e2e/agent-bundle-mcp-tools-docker.sh index 190dce14fc6..29c62e4e1fb 100755 --- a/scripts/e2e/pi-bundle-mcp-tools-docker.sh +++ b/scripts/e2e/agent-bundle-mcp-tools-docker.sh @@ -1,13 +1,13 @@ #!/usr/bin/env bash -# Verifies embedded Pi bundle MCP tool materialization and tool-policy behavior +# Verifies embedded OpenClaw bundle MCP tool materialization and tool-policy behavior # inside the package-installed functional E2E image. set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" -IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-pi-bundle-mcp-tools-e2e" OPENCLAW_IMAGE)" -CONTAINER_NAME="openclaw-pi-bundle-mcp-tools-e2e-$$" -RUN_LOG="$(mktemp -t openclaw-pi-bundle-mcp-tools-log.XXXXXX)" +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-agent-bundle-mcp-tools-e2e" OPENCLAW_IMAGE)" +CONTAINER_NAME="openclaw-agent-bundle-mcp-tools-e2e-$$" +RUN_LOG="$(mktemp -t openclaw-agent-bundle-mcp-tools-log.XXXXXX)" cleanup() { docker_e2e_docker_cmd rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true @@ -15,10 +15,10 @@ cleanup() { } trap cleanup EXIT -docker_e2e_build_or_reuse "$IMAGE_NAME" pi-bundle-mcp-tools -OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 pi-bundle-mcp-tools empty)" +docker_e2e_build_or_reuse "$IMAGE_NAME" agent-bundle-mcp-tools +OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 agent-bundle-mcp-tools empty)" -echo "Running in-container Pi bundle MCP tool availability smoke..." +echo "Running in-container OpenClaw bundle MCP tool availability smoke..." # Harness files are mounted read-only; the app under test comes from /app/dist. set +e docker_e2e_run_with_harness \ @@ -28,13 +28,13 @@ docker_e2e_run_with_harness \ bash -lc "set -euo pipefail source scripts/lib/openclaw-e2e-instance.sh openclaw_e2e_eval_test_state_from_b64 \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\" - tsx scripts/e2e/pi-bundle-mcp-tools-docker-client.ts + tsx scripts/e2e/agent-bundle-mcp-tools-docker-client.ts " >"$RUN_LOG" 2>&1 status=${PIPESTATUS[0]} set -e if [ "$status" -ne 0 ]; then - echo "Docker Pi bundle MCP tool availability smoke failed" + echo "Docker OpenClaw bundle MCP tool availability smoke failed" cat "$RUN_LOG" exit "$status" fi diff --git a/scripts/e2e/lib/fixtures/config.mjs b/scripts/e2e/lib/fixtures/config.mjs index 8b513d8eb90..0dbd578a5cf 100644 --- a/scripts/e2e/lib/fixtures/config.mjs +++ b/scripts/e2e/lib/fixtures/config.mjs @@ -101,7 +101,7 @@ function writeOpenWebUiConfig([openaiApiKey]) { path: "models.providers.openai.timeoutSeconds", value: Number.parseInt(process.env.OPENCLAW_OPENWEBUI_PROVIDER_TIMEOUT_SECONDS ?? "900", 10), }, - { path: "models.providers.openai.agentRuntime", value: { id: "pi" } }, + { path: "models.providers.openai.agentRuntime", value: { id: "openclaw" } }, { path: "gateway.controlUi.enabled", value: false }, { path: "gateway.mode", value: "local" }, { path: "gateway.bind", value: "lan" }, diff --git a/scripts/e2e/lib/live-plugin-tool/assertions.mjs b/scripts/e2e/lib/live-plugin-tool/assertions.mjs index c45d1cbe1e6..aa8fe7f080b 100644 --- a/scripts/e2e/lib/live-plugin-tool/assertions.mjs +++ b/scripts/e2e/lib/live-plugin-tool/assertions.mjs @@ -149,7 +149,7 @@ function configure() { api: "openai-responses", baseUrl: (process.env.OPENAI_BASE_URL || "https://api.openai.com/v1").trim(), apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, timeoutSeconds: agentTurnTimeoutSeconds, models: [ { @@ -176,7 +176,7 @@ function configure() { ...cfg.agents?.defaults?.models, [modelRef]: { ...cfg.agents?.defaults?.models?.[modelRef], - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, params: { transport: "sse", openaiWsWarmup: false }, }, }, diff --git a/scripts/e2e/lib/openai-chat-tools/write-config.mjs b/scripts/e2e/lib/openai-chat-tools/write-config.mjs index 0c3bd2ecd43..5d2c45afd3a 100644 --- a/scripts/e2e/lib/openai-chat-tools/write-config.mjs +++ b/scripts/e2e/lib/openai-chat-tools/write-config.mjs @@ -42,7 +42,7 @@ const config = { api: "openai-responses", apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, baseUrl: (process.env.OPENAI_BASE_URL || "https://api.openai.com/v1").trim(), - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, timeoutSeconds, models: [ { @@ -65,7 +65,7 @@ const config = { model: { primary: modelRef, fallbacks: [] }, models: { [modelRef]: { - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, params: { transport: "sse", openaiWsWarmup: false }, }, }, diff --git a/scripts/e2e/npm-telegram-rtt-config.mjs b/scripts/e2e/npm-telegram-rtt-config.mjs index a6fb353755a..a6b1b618c44 100755 --- a/scripts/e2e/npm-telegram-rtt-config.mjs +++ b/scripts/e2e/npm-telegram-rtt-config.mjs @@ -33,7 +33,7 @@ config.models = config.models ?? {}; config.models.providers = config.models.providers ?? {}; config.models.providers.openai = { api: "openai-responses", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, apiKey: { source: "env", provider: "default", @@ -56,7 +56,7 @@ config.agents.defaults = config.agents.defaults ?? {}; config.agents.defaults.model = { primary: "openai/gpt-5.5" }; config.agents.defaults.models = { "openai/gpt-5.5": { - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, params: { transport: "sse", openaiWsWarmup: false, diff --git a/scripts/e2e/parallels/powershell.ts b/scripts/e2e/parallels/powershell.ts index 3e7c0de0a1e..64cd43d6d11 100644 --- a/scripts/e2e/parallels/powershell.ts +++ b/scripts/e2e/parallels/powershell.ts @@ -138,7 +138,7 @@ for (const op of payload.operations || []) { const selectedModelEntry = cfg.agents.defaults.models[payload.modelId]; if (selectedModelEntry && typeof selectedModelEntry === "object" && !Array.isArray(selectedModelEntry)) { if (canWriteAgentRuntime) { - selectedModelEntry.agentRuntime = { id: "pi" }; + selectedModelEntry.agentRuntime = { id: "openclaw" }; } else { delete selectedModelEntry.agentRuntime; } diff --git a/scripts/e2e/session-runtime-context-docker-client.ts b/scripts/e2e/session-runtime-context-docker-client.ts index 497ceed3464..ce65ea2fc91 100644 --- a/scripts/e2e/session-runtime-context-docker-client.ts +++ b/scripts/e2e/session-runtime-context-docker-client.ts @@ -5,11 +5,11 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import { queueRuntimeContextForNextTurn, resolveRuntimeContextPromptParts, -} from "../../dist/agents/pi-embedded-runner/run/runtime-context-prompt.js"; +} from "../../dist/agents/embedded-agent-runner/run/runtime-context-prompt.js"; type TranscriptEntry = { type?: string; diff --git a/scripts/embedded-run-abort-leak.ts b/scripts/embedded-run-abort-leak.ts index d97ebead535..4c90a3888ba 100644 --- a/scripts/embedded-run-abort-leak.ts +++ b/scripts/embedded-run-abort-leak.ts @@ -19,7 +19,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import * as v8 from "node:v8"; -import { abortable as productionAbortable } from "../src/agents/pi-embedded-runner/run/abortable.js"; +import { abortable as productionAbortable } from "../src/agents/embedded-agent-runner/run/abortable.js"; type Mode = "production" | "closure-extracted" | "closure-inline" | "synthetic-leak"; diff --git a/scripts/lib/ci-node-test-plan.mjs b/scripts/lib/ci-node-test-plan.mjs index cc6e8da0974..e1857e78637 100644 --- a/scripts/lib/ci-node-test-plan.mjs +++ b/scripts/lib/ci-node-test-plan.mjs @@ -362,7 +362,7 @@ const SPLIT_NODE_SHARDS = new Map([ shardName: "agentic-agents", configs: [ "test/vitest/vitest.agents-core.config.ts", - "test/vitest/vitest.agents-pi-embedded.config.ts", + "test/vitest/vitest.agents-embedded-agent.config.ts", "test/vitest/vitest.agents-support.config.ts", "test/vitest/vitest.agents-tools.config.ts", ], diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index 606fc442965..671aff055f7 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -386,9 +386,13 @@ export const mainLanes = [ stateScenario: "empty", weight: 3, }), - lane("pi-bundle-mcp-tools", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:pi-bundle-mcp-tools", { - stateScenario: "empty", - }), + lane( + "agent-bundle-mcp-tools", + "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:agent-bundle-mcp-tools", + { + stateScenario: "empty", + }, + ), lane("crestodian-rescue", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:crestodian-rescue", { stateScenario: "empty", }), @@ -735,8 +739,8 @@ const primaryReleasePathChunks = { stateScenario: "empty", }), lane( - "pi-bundle-mcp-tools", - "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:pi-bundle-mcp-tools", + "agent-bundle-mcp-tools", + "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:agent-bundle-mcp-tools", { stateScenario: "empty" }, ), serviceLane("mcp-channels", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:mcp-channels", { diff --git a/scripts/lib/openclaw-e2e-instance.sh b/scripts/lib/openclaw-e2e-instance.sh index 4bb17f6564c..c3004bb603c 100644 --- a/scripts/lib/openclaw-e2e-instance.sh +++ b/scripts/lib/openclaw-e2e-instance.sh @@ -216,7 +216,6 @@ openclaw_e2e_write_state_env() { printf 'export OPENCLAW_STATE_DIR=%q\n' "$OPENCLAW_STATE_DIR" printf 'export OPENCLAW_CONFIG_PATH=%q\n' "$OPENCLAW_CONFIG_PATH" printf 'export OPENCLAW_AGENT_DIR=%q\n' "${OPENCLAW_AGENT_DIR-}" - printf 'export PI_CODING_AGENT_DIR=%q\n' "${PI_CODING_AGENT_DIR-}" } >"$target" } openclaw_e2e_install_trash_shim() { diff --git a/scripts/lib/openclaw-test-state.mjs b/scripts/lib/openclaw-test-state.mjs index c39e374475e..2111de02a94 100644 --- a/scripts/lib/openclaw-test-state.mjs +++ b/scripts/lib/openclaw-test-state.mjs @@ -404,7 +404,6 @@ export function renderShellFunction() { ${renderAuthProfileSecretKeyExport().join("\n ")} export OPENCLAW_TEST_WORKSPACE_DIR="$OPENCLAW_TEST_STATE_HOME/workspace" unset OPENCLAW_AGENT_DIR - unset PI_CODING_AGENT_DIR unset OPENCLAW_SERVICE_REPAIR_POLICY mkdir -p "$OPENCLAW_STATE_DIR" "$OPENCLAW_TEST_WORKSPACE_DIR" case "$scenario" in diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index b655a45b766..3c051f91aac 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -317,5 +317,16 @@ "webhook-path", "web-media", "zalouser", - "zod" + "zod", + "agent-core", + "agent-sessions", + "llm", + "llm-anthropic", + "llm-bedrock", + "llm-google-shared", + "llm-provider-runtime", + "llm-oauth", + "llm-openai-codex-responses", + "llm-openai-completions", + "llm-openai-responses" ] diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index 160d8cf9d45..9e0f625a64e 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -101,7 +101,7 @@ function buildReleaseProviderConfigOverride(providerMeta) { } return { ...(typeof providerMeta.baseUrl === "string" ? { baseUrl: providerMeta.baseUrl } : {}), - ...(providerMeta.extensionId === "openai" ? { agentRuntime: { id: "pi" } } : {}), + ...(providerMeta.extensionId === "openai" ? { agentRuntime: { id: "openclaw" } } : {}), models: [], ...(typeof providerMeta.timeoutSeconds === "number" ? { timeoutSeconds: providerMeta.timeoutSeconds } diff --git a/scripts/package-mac-app.sh b/scripts/package-mac-app.sh index 8c87eb51e26..c522176d65d 100755 --- a/scripts/package-mac-app.sh +++ b/scripts/package-mac-app.sh @@ -281,7 +281,7 @@ rm -rf "$APP_ROOT/Contents/Resources/DeviceModels" cp -R "$ROOT_DIR/apps/macos/Sources/OpenClaw/Resources/DeviceModels" "$APP_ROOT/Contents/Resources/DeviceModels" echo "📦 Copying model catalog" -MODEL_CATALOG_SRC="$ROOT_DIR/node_modules/@earendil-works/pi-ai/dist/models.generated.js" +MODEL_CATALOG_SRC="$ROOT_DIR/src/llm/models.generated.ts" MODEL_CATALOG_DEST="$APP_ROOT/Contents/Resources/models.generated.js" if [ -f "$MODEL_CATALOG_SRC" ]; then cp "$MODEL_CATALOG_SRC" "$MODEL_CATALOG_DEST" diff --git a/scripts/perf/issue-78851-model-resolution.ts b/scripts/perf/issue-78851-model-resolution.ts index 8ce3ba19937..5bf974c30f1 100644 --- a/scripts/perf/issue-78851-model-resolution.ts +++ b/scripts/perf/issue-78851-model-resolution.ts @@ -3,11 +3,11 @@ import * as inspector from "node:inspector"; import { tmpdir } from "node:os"; import path from "node:path"; import { monitorEventLoopDelay, performance } from "node:perf_hooks"; +import { resolveModelAsync } from "../../src/agents/embedded-agent-runner/model.js"; import { ensureOpenClawModelsJson, resetModelsJsonReadyCacheForTest, } from "../../src/agents/models-config.js"; -import { resolveModelAsync } from "../../src/agents/pi-embedded-runner/model.js"; import type { OpenClawConfig } from "../../src/config/types.openclaw.js"; type Options = { diff --git a/scripts/perf/rtt-regression-audit.md b/scripts/perf/rtt-regression-audit.md index 476bf7bdbcc..18d32ba399c 100644 --- a/scripts/perf/rtt-regression-audit.md +++ b/scripts/perf/rtt-regression-audit.md @@ -49,7 +49,7 @@ Status: branch-local checkpoint, not release notes. - After rebasing onto `b5046968f61`, a fresh Testbox `pnpm check:changed` attempt on `tbx_01krwbsg15xvjdgpcz8fxq1htz` was blocked before reaching the changed gate: pnpm install rejected newly published - `@earendil-works/pi-ai@0.74.1` under `minimumReleaseAge`. + `openclaw/plugin-sdk/llm@0.74.1` under `minimumReleaseAge`. - After rebasing again, Testbox-through-Crabbox `tbx_01krwcxpxx1n22t8jmvcj40228` ran `pnpm check:changed` with an explicit `origin/main` fetch to repair the diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 9e922bbf117..f4996abc16c 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -55,7 +55,8 @@ import { const DEFAULT_VITEST_CONFIG = "test/vitest/vitest.unit.config.ts"; const AGENTS_CORE_VITEST_CONFIG = "test/vitest/vitest.agents-core.config.ts"; -const AGENTS_PI_EMBEDDED_VITEST_CONFIG = "test/vitest/vitest.agents-pi-embedded.config.ts"; +const AGENTS_EMBEDDED_AGENT_VITEST_CONFIG = + "test/vitest/vitest.agents-embedded-agent.config.ts"; const AGENTS_SUPPORT_VITEST_CONFIG = "test/vitest/vitest.agents-support.config.ts"; const AGENTS_TOOLS_VITEST_CONFIG = "test/vitest/vitest.agents-tools.config.ts"; const AGENTS_VITEST_CONFIG = "test/vitest/vitest.agents.config.ts"; @@ -140,7 +141,7 @@ const FULL_SUITE_CONFIG_WEIGHT = new Map([ [GATEWAY_METHODS_VITEST_CONFIG, 177], [COMMANDS_VITEST_CONFIG, 175], [AGENTS_CORE_VITEST_CONFIG, 170], - [AGENTS_PI_EMBEDDED_VITEST_CONFIG, 169], + [AGENTS_EMBEDDED_AGENT_VITEST_CONFIG, 169], [AGENTS_SUPPORT_VITEST_CONFIG, 168], [AGENTS_TOOLS_VITEST_CONFIG, 167], [EXTENSION_CODEX_VITEST_CONFIG, 168], @@ -248,7 +249,7 @@ const CHANGED_ARGS_PATTERN = /^--changed(?:=(.+))?$/u; const VITEST_CONFIG_BY_KIND = { acp: ACP_VITEST_CONFIG, agentCore: AGENTS_CORE_VITEST_CONFIG, - agentPiEmbedded: AGENTS_PI_EMBEDDED_VITEST_CONFIG, + agentEmbedded: AGENTS_EMBEDDED_AGENT_VITEST_CONFIG, agentSupport: AGENTS_SUPPORT_VITEST_CONFIG, agentTools: AGENTS_TOOLS_VITEST_CONFIG, agent: AGENTS_VITEST_CONFIG, diff --git a/src/agents/pi-auth-credentials.ts b/src/agents/agent-auth-credentials.ts similarity index 70% rename from src/agents/pi-auth-credentials.ts rename to src/agents/agent-auth-credentials.ts index 5f8ef67c17e..d47e4cc12b5 100644 --- a/src/agents/pi-auth-credentials.ts +++ b/src/agents/agent-auth-credentials.ts @@ -3,40 +3,40 @@ import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { AuthProfileCredential, AuthProfileStore } from "./auth-profiles.js"; import { normalizeProviderId } from "./provider-id.js"; -type PiApiKeyCredential = { type: "api_key"; key: string }; -type PiOAuthCredential = { +type AgentApiKeyCredential = { type: "api_key"; key: string }; +type AgentOAuthCredential = { type: "oauth"; access: string; refresh: string; expires: number; }; -export type PiCredential = PiApiKeyCredential | PiOAuthCredential; -export type PiCredentialMap = Record; +export type AgentCredential = AgentApiKeyCredential | AgentOAuthCredential; +export type AgentCredentialMap = Record; -export type ResolvePiCredentialMapOptions = { +export type ResolveAgentCredentialMapOptions = { includeSecretRefPlaceholders?: boolean; }; -const PI_SECRET_REF_CONFIGURED_MARKER = "openclaw-secret-ref-configured"; +const AGENT_SECRET_REF_CONFIGURED_MARKER = "openclaw-secret-ref-configured"; function hasConfiguredSecretRef(value: unknown): boolean { return coerceSecretRef(value) !== null; } function secretRefPlaceholder( - options: ResolvePiCredentialMapOptions | undefined, -): PiCredential | null { + options: ResolveAgentCredentialMapOptions | undefined, +): AgentCredential | null { if (options?.includeSecretRefPlaceholders === true) { - return { type: "api_key", key: PI_SECRET_REF_CONFIGURED_MARKER }; + return { type: "api_key", key: AGENT_SECRET_REF_CONFIGURED_MARKER }; } return null; } -function convertAuthProfileCredentialToPi( +function convertAuthProfileCredentialToAgent( cred: AuthProfileCredential, - options?: ResolvePiCredentialMapOptions, -): PiCredential | null { + options?: ResolveAgentCredentialMapOptions, +): AgentCredential | null { if (cred.type === "api_key") { const key = normalizeOptionalString(cred.key) ?? ""; if (!key) { @@ -77,17 +77,17 @@ function convertAuthProfileCredentialToPi( return null; } -export function resolvePiCredentialMapFromStore( +export function resolveAgentCredentialMapFromStore( store: AuthProfileStore, - options?: ResolvePiCredentialMapOptions, -): PiCredentialMap { - const credentials: PiCredentialMap = {}; + options?: ResolveAgentCredentialMapOptions, +): AgentCredentialMap { + const credentials: AgentCredentialMap = {}; for (const credential of Object.values(store.profiles)) { const provider = normalizeProviderId(credential.provider ?? ""); if (!provider || credentials[provider]) { continue; } - const converted = convertAuthProfileCredentialToPi(credential, options); + const converted = convertAuthProfileCredentialToAgent(credential, options); if (converted) { credentials[provider] = converted; } @@ -95,7 +95,7 @@ export function resolvePiCredentialMapFromStore( return credentials; } -export function piCredentialsEqual(a: PiCredential | undefined, b: PiCredential): boolean { +export function agentCredentialsEqual(a: AgentCredential | undefined, b: AgentCredential): boolean { if (!a || typeof a !== "object") { return false; } diff --git a/src/agents/pi-auth-discovery-core.ts b/src/agents/agent-auth-discovery-core.ts similarity index 86% rename from src/agents/pi-auth-discovery-core.ts rename to src/agents/agent-auth-discovery-core.ts index 228e5c75440..23ec78ef179 100644 --- a/src/agents/pi-auth-discovery-core.ts +++ b/src/agents/agent-auth-discovery-core.ts @@ -3,23 +3,23 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { tryReadJsonSync } from "../infra/json-files.js"; import { replaceFileAtomicSync } from "../infra/replace-file.js"; import { isRecord } from "../utils.js"; +import type { AgentCredentialMap } from "./agent-auth-credentials.js"; import { listProviderEnvAuthLookupKeys, resolveProviderEnvAuthLookupMaps, } from "./model-auth-env-vars.js"; import { resolveEnvApiKey } from "./model-auth-env.js"; -import type { PiCredentialMap } from "./pi-auth-credentials.js"; -export type PiDiscoveryAuthLookupOptions = { +export type AgentDiscoveryAuthLookupOptions = { config?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; }; -export function addEnvBackedPiCredentials( - credentials: PiCredentialMap, - options: PiDiscoveryAuthLookupOptions = {}, -): PiCredentialMap { +export function addEnvBackedAgentCredentials( + credentials: AgentCredentialMap, + options: AgentDiscoveryAuthLookupOptions = {}, +): AgentCredentialMap { const env = options.env ?? process.env; const lookupParams = { config: options.config, @@ -29,7 +29,7 @@ export function addEnvBackedPiCredentials( const lookupMaps = resolveProviderEnvAuthLookupMaps(lookupParams); const { aliasMap, envCandidateMap: candidateMap, authEvidenceMap } = lookupMaps; const next = { ...credentials }; - // pi-coding-agent hides providers from its registry when auth storage lacks + // session runtime hides providers from its registry when auth storage lacks // a matching credential entry. Mirror env-backed provider auth here so // live/model discovery sees the same providers runtime auth can use. for (const provider of listProviderEnvAuthLookupKeys({ @@ -96,6 +96,6 @@ export function scrubLegacyStaticAuthJsonEntriesForDiscovery(pathname: string): content: `${JSON.stringify(parsed, null, 2)}\n`, dirMode: 0o700, mode: 0o600, - tempPrefix: ".pi-auth", + tempPrefix: ".agent-auth", }); } diff --git a/src/agents/pi-auth-discovery.external-cli.test.ts b/src/agents/agent-auth-discovery.external-cli.test.ts similarity index 84% rename from src/agents/pi-auth-discovery.external-cli.test.ts rename to src/agents/agent-auth-discovery.external-cli.test.ts index 17ce8b1b2a4..7b4a9daca26 100644 --- a/src/agents/pi-auth-discovery.external-cli.test.ts +++ b/src/agents/agent-auth-discovery.external-cli.test.ts @@ -10,11 +10,11 @@ const storeMocks = vi.hoisted(() => ({ })); const credentialMocks = vi.hoisted(() => ({ - resolvePiCredentialMapFromStore: vi.fn(() => ({})), + resolveAgentCredentialMapFromStore: vi.fn(() => ({})), })); const discoveryCoreMocks = vi.hoisted(() => ({ - addEnvBackedPiCredentials: vi.fn((credentials: unknown) => credentials), + addEnvBackedAgentCredentials: vi.fn((credentials: unknown) => credentials), scrubLegacyStaticAuthJsonEntriesForDiscovery: vi.fn(), })); @@ -25,9 +25,9 @@ const syntheticAuthMocks = vi.hoisted(() => ({ vi.mock("./auth-profiles/store.js", () => storeMocks); -vi.mock("./pi-auth-credentials.js", () => credentialMocks); +vi.mock("./agent-auth-credentials.js", () => credentialMocks); -vi.mock("./pi-auth-discovery-core.js", () => discoveryCoreMocks); +vi.mock("./agent-auth-discovery-core.js", () => discoveryCoreMocks); vi.mock("./synthetic-auth.runtime.js", () => ({ resolveRuntimeSyntheticAuthProviderRefs: @@ -38,10 +38,10 @@ vi.mock("../plugins/provider-runtime.js", () => ({ resolveProviderSyntheticAuthWithPlugin: syntheticAuthMocks.resolveProviderSyntheticAuthWithPlugin, })); +import { resolveAgentCredentialsForDiscovery } from "./agent-auth-discovery.js"; import { externalCliDiscoveryForProviders } from "./auth-profiles/external-cli-discovery.js"; -import { resolvePiCredentialsForDiscovery } from "./pi-auth-discovery.js"; -describe("resolvePiCredentialsForDiscovery external CLI scoping", () => { +describe("resolveAgentCredentialsForDiscovery external CLI scoping", () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -53,7 +53,7 @@ describe("resolvePiCredentialsForDiscovery external CLI scoping", () => { providers: ["fireworks"], }); - resolvePiCredentialsForDiscovery("/tmp/openclaw-agent", { + resolveAgentCredentialsForDiscovery("/tmp/openclaw-agent", { config: cfg, env: {}, externalCli, @@ -74,7 +74,7 @@ describe("resolvePiCredentialsForDiscovery external CLI scoping", () => { providers: ["fireworks"], }); - resolvePiCredentialsForDiscovery("/tmp/openclaw-agent", { + resolveAgentCredentialsForDiscovery("/tmp/openclaw-agent", { config: cfg, env: {}, externalCli, @@ -90,7 +90,7 @@ describe("resolvePiCredentialsForDiscovery external CLI scoping", () => { }); it("can skip runtime external auth overlays and scope synthetic auth discovery", () => { - resolvePiCredentialsForDiscovery("/tmp/openclaw-agent", { + resolveAgentCredentialsForDiscovery("/tmp/openclaw-agent", { env: {}, skipExternalAuthProfiles: true, syntheticAuthProviderRefs: ["fireworks"], diff --git a/src/agents/pi-auth-discovery.ts b/src/agents/agent-auth-discovery.ts similarity index 82% rename from src/agents/pi-auth-discovery.ts rename to src/agents/agent-auth-discovery.ts index ce55b2b14b9..041330b9582 100644 --- a/src/agents/pi-auth-discovery.ts +++ b/src/agents/agent-auth-discovery.ts @@ -1,5 +1,13 @@ import { resolveProviderSyntheticAuthWithPlugin } from "../plugins/provider-runtime.js"; import { resolveRuntimeSyntheticAuthProviderRefs } from "../plugins/synthetic-auth.runtime.js"; +import { + resolveAgentCredentialMapFromStore, + type AgentCredentialMap, +} from "./agent-auth-credentials.js"; +import { + addEnvBackedAgentCredentials, + type AgentDiscoveryAuthLookupOptions, +} from "./agent-auth-discovery-core.js"; import type { ExternalCliAuthDiscovery } from "./auth-profiles/external-cli-discovery.js"; import { ensureAuthProfileStore, @@ -8,11 +16,6 @@ import { loadAuthProfileStoreForRuntime, loadAuthProfileStoreForSecretsRuntime, } from "./auth-profiles/store.js"; -import { resolvePiCredentialMapFromStore, type PiCredentialMap } from "./pi-auth-credentials.js"; -import { - addEnvBackedPiCredentials, - type PiDiscoveryAuthLookupOptions, -} from "./pi-auth-discovery-core.js"; export type DiscoverAuthStorageOptions = { externalCli?: ExternalCliAuthDiscovery; @@ -20,12 +23,12 @@ export type DiscoverAuthStorageOptions = { skipExternalAuthProfiles?: boolean; skipCredentials?: boolean; syntheticAuthProviderRefs?: Iterable; -} & PiDiscoveryAuthLookupOptions; +} & AgentDiscoveryAuthLookupOptions; -export function resolvePiCredentialsForDiscovery( +export function resolveAgentCredentialsForDiscovery( agentDir: string, options?: DiscoverAuthStorageOptions, -): PiCredentialMap { +): AgentCredentialMap { const storeOptions = { allowKeychainPrompt: false, ...(options?.config ? { config: options.config } : {}), @@ -43,8 +46,8 @@ export function resolvePiCredentialsForDiscovery( ? loadAuthProfileStoreForRuntime(agentDir, { readOnly: true, ...storeOptions }) : loadAuthProfileStoreForSecretsRuntime(agentDir) : ensureAuthProfileStore(agentDir, storeOptions); - const credentials = addEnvBackedPiCredentials( - resolvePiCredentialMapFromStore(store, { + const credentials = addEnvBackedAgentCredentials( + resolveAgentCredentialMapFromStore(store, { includeSecretRefPlaceholders: options?.readOnly === true, }), { @@ -80,6 +83,6 @@ export function resolvePiCredentialsForDiscovery( } export { - addEnvBackedPiCredentials, + addEnvBackedAgentCredentials, scrubLegacyStaticAuthJsonEntriesForDiscovery, -} from "./pi-auth-discovery-core.js"; +} from "./agent-auth-discovery-core.js"; diff --git a/src/agents/pi-auth-json.test.ts b/src/agents/agent-auth-json.test.ts similarity index 87% rename from src/agents/pi-auth-json.test.ts rename to src/agents/agent-auth-json.test.ts index 2b86170d037..9fbabf3ac25 100644 --- a/src/agents/pi-auth-json.test.ts +++ b/src/agents/agent-auth-json.test.ts @@ -2,8 +2,8 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; +import { ensureAgentAuthJsonFromAuthProfiles } from "./agent-auth-json.js"; import { saveAuthProfileStore } from "./auth-profiles/store.js"; -import { ensurePiAuthJsonFromAuthProfiles } from "./pi-auth-json.js"; vi.mock("./auth-profiles/external-auth.js", () => ({ listRuntimeExternalAuthProfiles: () => [], @@ -64,8 +64,8 @@ function expectOAuthAuth( } } -describe("ensurePiAuthJsonFromAuthProfiles", () => { - it("writes openai-codex oauth credentials into auth.json for pi-coding-agent discovery", async () => { +describe("ensureAgentAuthJsonFromAuthProfiles", () => { + it("writes openai-codex oauth credentials into auth.json for session runtime discovery", async () => { const agentDir = await createAgentDir(); writeProfiles(agentDir, { @@ -78,13 +78,13 @@ describe("ensurePiAuthJsonFromAuthProfiles", () => { }, }); - const first = await ensurePiAuthJsonFromAuthProfiles(agentDir); + const first = await ensureAgentAuthJsonFromAuthProfiles(agentDir); expect(first.wrote).toBe(true); const auth = await readAuthJson(agentDir); expectOAuthAuth(auth, "openai-codex", "access-token", "refresh-token"); - const second = await ensurePiAuthJsonFromAuthProfiles(agentDir); + const second = await ensureAgentAuthJsonFromAuthProfiles(agentDir); expect(second.wrote).toBe(false); }); @@ -99,7 +99,7 @@ describe("ensurePiAuthJsonFromAuthProfiles", () => { }, }); - const result = await ensurePiAuthJsonFromAuthProfiles(agentDir); + const result = await ensureAgentAuthJsonFromAuthProfiles(agentDir); expect(result.wrote).toBe(true); const auth = await readAuthJson(agentDir); @@ -117,7 +117,7 @@ describe("ensurePiAuthJsonFromAuthProfiles", () => { }, }); - const result = await ensurePiAuthJsonFromAuthProfiles(agentDir); + const result = await ensureAgentAuthJsonFromAuthProfiles(agentDir); expect(result.wrote).toBe(true); const auth = await readAuthJson(agentDir); @@ -147,7 +147,7 @@ describe("ensurePiAuthJsonFromAuthProfiles", () => { }, }); - const result = await ensurePiAuthJsonFromAuthProfiles(agentDir); + const result = await ensureAgentAuthJsonFromAuthProfiles(agentDir); expect(result.wrote).toBe(true); const auth = await readAuthJson(agentDir); @@ -168,7 +168,7 @@ describe("ensurePiAuthJsonFromAuthProfiles", () => { }, }); - const result = await ensurePiAuthJsonFromAuthProfiles(agentDir); + const result = await ensureAgentAuthJsonFromAuthProfiles(agentDir); expect(result.wrote).toBe(false); }); @@ -184,7 +184,7 @@ describe("ensurePiAuthJsonFromAuthProfiles", () => { }, }); - const result = await ensurePiAuthJsonFromAuthProfiles(agentDir); + const result = await ensureAgentAuthJsonFromAuthProfiles(agentDir); expect(result.wrote).toBe(false); }); @@ -199,7 +199,7 @@ describe("ensurePiAuthJsonFromAuthProfiles", () => { }, }); - const result = await ensurePiAuthJsonFromAuthProfiles(agentDir); + const result = await ensureAgentAuthJsonFromAuthProfiles(agentDir); expect(result.wrote).toBe(true); const auth = await readAuthJson(agentDir); @@ -225,7 +225,7 @@ describe("ensurePiAuthJsonFromAuthProfiles", () => { }, }); - await ensurePiAuthJsonFromAuthProfiles(agentDir); + await ensureAgentAuthJsonFromAuthProfiles(agentDir); const auth = await readAuthJson(agentDir); expectApiKeyAuth(auth, "legacy-provider", "legacy-key"); @@ -247,7 +247,7 @@ describe("ensurePiAuthJsonFromAuthProfiles", () => { }, }); - const result = await ensurePiAuthJsonFromAuthProfiles(agentDir); + const result = await ensureAgentAuthJsonFromAuthProfiles(agentDir); expect(result.wrote).toBe(true); const auth = await readAuthJson(agentDir); diff --git a/src/agents/pi-auth-json.ts b/src/agents/agent-auth-json.ts similarity index 74% rename from src/agents/pi-auth-json.ts rename to src/agents/agent-auth-json.ts index 16f9a1fb082..b54d41e24be 100644 --- a/src/agents/pi-auth-json.ts +++ b/src/agents/agent-auth-json.ts @@ -2,16 +2,16 @@ import path from "node:path"; import { z } from "zod"; import { privateFileStore } from "../infra/private-file-store.js"; import { safeParseWithSchema } from "../utils/zod-parse.js"; -import { ensureAuthProfileStore } from "./auth-profiles/store.js"; import { - piCredentialsEqual, - resolvePiCredentialMapFromStore, - type PiCredential, -} from "./pi-auth-credentials.js"; + agentCredentialsEqual, + resolveAgentCredentialMapFromStore, + type AgentCredential, +} from "./agent-auth-credentials.js"; +import { ensureAuthProfileStore } from "./auth-profiles/store.js"; type AuthJsonShape = Record; -const PiCredentialSchema: z.ZodType = z.discriminatedUnion("type", [ +const AgentCredentialSchema: z.ZodType = z.discriminatedUnion("type", [ z.object({ type: z.literal("api_key"), key: z.string(), @@ -38,10 +38,10 @@ async function readAuthJson(rootDir: string, filePath: string): Promise { const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false }); const authPath = path.join(agentDir, "auth.json"); - const providerCredentials = resolvePiCredentialMapFromStore(store); + const providerCredentials = resolveAgentCredentialMapFromStore(store); if (Object.keys(providerCredentials).length === 0) { return { wrote: false, authPath }; } @@ -64,8 +64,8 @@ export async function ensurePiAuthJsonFromAuthProfiles(agentDir: string): Promis let changed = false; for (const [provider, cred] of Object.entries(providerCredentials)) { - const current = safeParseWithSchema(PiCredentialSchema, existing[provider]) ?? undefined; - if (!piCredentialsEqual(current, cred)) { + const current = safeParseWithSchema(AgentCredentialSchema, existing[provider]) ?? undefined; + if (!agentCredentialsEqual(current, cred)) { existing[provider] = cred; changed = true; } diff --git a/src/agents/pi-bundle-lsp-runtime.test.ts b/src/agents/agent-bundle-lsp-runtime.test.ts similarity index 88% rename from src/agents/pi-bundle-lsp-runtime.test.ts rename to src/agents/agent-bundle-lsp-runtime.test.ts index 7ba6789ec61..956a7c8aa37 100644 --- a/src/agents/pi-bundle-lsp-runtime.test.ts +++ b/src/agents/agent-bundle-lsp-runtime.test.ts @@ -4,7 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; const spawnMock = vi.hoisted(() => vi.fn()); const killProcessTreeMock = vi.hoisted(() => vi.fn()); -const loadEmbeddedPiLspConfigMock = vi.hoisted(() => vi.fn()); +const loadEmbeddedAgentLspConfigMock = vi.hoisted(() => vi.fn()); vi.mock("node:child_process", async () => ({ ...(await vi.importActual("node:child_process")), @@ -15,8 +15,8 @@ vi.mock("../process/kill-tree.js", () => ({ killProcessTree: killProcessTreeMock, })); -vi.mock("./embedded-pi-lsp.js", () => ({ - loadEmbeddedPiLspConfig: loadEmbeddedPiLspConfigMock, +vi.mock("./embedded-agent-lsp.js", () => ({ + loadEmbeddedAgentLspConfig: loadEmbeddedAgentLspConfigMock, })); vi.mock("../logger.js", () => ({ @@ -77,7 +77,7 @@ class MockChildProcess extends EventEmitter { } function configureSingleLspServer(): void { - loadEmbeddedPiLspConfigMock.mockReturnValue({ + loadEmbeddedAgentLspConfigMock.mockReturnValue({ lspServers: { typescript: { command: "typescript-language-server", @@ -90,18 +90,18 @@ function configureSingleLspServer(): void { describe("bundle LSP runtime", () => { afterEach(async () => { - const { disposeAllBundleLspRuntimes } = await import("./pi-bundle-lsp-runtime.js"); + const { disposeAllBundleLspRuntimes } = await import("./agent-bundle-lsp-runtime.js"); await disposeAllBundleLspRuntimes(); spawnMock.mockReset(); killProcessTreeMock.mockReset(); - loadEmbeddedPiLspConfigMock.mockReset(); + loadEmbeddedAgentLspConfigMock.mockReset(); }); it("starts LSP servers in a disposable process group", async () => { configureSingleLspServer(); const child = new MockChildProcess(); spawnMock.mockReturnValue(child); - const { createBundleLspToolRuntime } = await import("./pi-bundle-lsp-runtime.js"); + const { createBundleLspToolRuntime } = await import("./agent-bundle-lsp-runtime.js"); const runtime = await createBundleLspToolRuntime({ workspaceDir: "/tmp/workspace" }); @@ -124,7 +124,7 @@ describe("bundle LSP runtime", () => { const child = new MockChildProcess(); spawnMock.mockReturnValue(child); const { createBundleLspToolRuntime, disposeAllBundleLspRuntimes } = - await import("./pi-bundle-lsp-runtime.js"); + await import("./agent-bundle-lsp-runtime.js"); const runtime = await createBundleLspToolRuntime({ workspaceDir: "/tmp/workspace" }); diff --git a/src/agents/pi-bundle-lsp-runtime.ts b/src/agents/agent-bundle-lsp-runtime.ts similarity index 98% rename from src/agents/pi-bundle-lsp-runtime.ts rename to src/agents/agent-bundle-lsp-runtime.ts index f4323846528..049056ea42b 100644 --- a/src/agents/pi-bundle-lsp-runtime.ts +++ b/src/agents/agent-bundle-lsp-runtime.ts @@ -1,5 +1,4 @@ import { spawn, type ChildProcess } from "node:child_process"; -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { sanitizeHostExecEnv } from "../infra/host-env-security.js"; import { logDebug, logWarn } from "../logger.js"; @@ -10,12 +9,13 @@ import { import { setPluginToolMeta } from "../plugins/tools.js"; import { killProcessTree } from "../process/kill-tree.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; -import { loadEmbeddedPiLspConfig } from "./embedded-pi-lsp.js"; +import { loadEmbeddedAgentLspConfig } from "./embedded-agent-lsp.js"; import { resolveStdioMcpServerLaunchConfig, describeStdioMcpServerLaunchConfig, type StdioMcpServerLaunchConfig, } from "./mcp-stdio.js"; +import type { AgentToolResult } from "./runtime/index.js"; import type { AnyAgentTool } from "./tools/common.js"; // Minimal LSP JSON-RPC framing over stdio (Content-Length header + JSON body). @@ -395,7 +395,7 @@ export async function createBundleLspToolRuntime(params: { cfg?: OpenClawConfig; reservedToolNames?: Iterable; }): Promise { - const loaded = loadEmbeddedPiLspConfig({ + const loaded = loadEmbeddedAgentLspConfig({ workspaceDir: params.workspaceDir, cfg: params.cfg, }); diff --git a/src/agents/pi-bundle-lsp-runtime.windows-spawn.test.ts b/src/agents/agent-bundle-lsp-runtime.windows-spawn.test.ts similarity index 95% rename from src/agents/pi-bundle-lsp-runtime.windows-spawn.test.ts rename to src/agents/agent-bundle-lsp-runtime.windows-spawn.test.ts index 5afbd37f86f..afe7a65fd2f 100644 --- a/src/agents/pi-bundle-lsp-runtime.windows-spawn.test.ts +++ b/src/agents/agent-bundle-lsp-runtime.windows-spawn.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi, beforeEach } from "vitest"; -import { spawnLspServerProcess } from "./pi-bundle-lsp-runtime.js"; +import { spawnLspServerProcess } from "./agent-bundle-lsp-runtime.js"; const resolveWindowsSpawnProgramMock = vi.hoisted(() => vi.fn()); const materializeWindowsSpawnProgramMock = vi.hoisted(() => vi.fn()); @@ -29,8 +29,8 @@ vi.mock("../process/kill-tree.js", () => ({ killProcessTree: vi.fn(), })); -vi.mock("./embedded-pi-lsp.js", () => ({ - loadEmbeddedPiLspConfig: vi.fn().mockReturnValue({ lspServers: {}, diagnostics: [] }), +vi.mock("./embedded-agent-lsp.js", () => ({ + loadEmbeddedAgentLspConfig: vi.fn().mockReturnValue({ lspServers: {}, diagnostics: [] }), })); const FAKE_CHILD = { diff --git a/src/agents/pi-bundle-mcp-materialize.ts b/src/agents/agent-bundle-mcp-materialize.ts similarity index 94% rename from src/agents/pi-bundle-mcp-materialize.ts rename to src/agents/agent-bundle-mcp-materialize.ts index d3cdeba0bce..f750d128a2d 100644 --- a/src/agents/pi-bundle-mcp-materialize.ts +++ b/src/agents/agent-bundle-mcp-materialize.ts @@ -1,5 +1,4 @@ import crypto from "node:crypto"; -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { logWarn } from "../logger.js"; @@ -9,9 +8,10 @@ import { buildSafeToolName, normalizeReservedToolNames, TOOL_NAME_SEPARATOR, -} from "./pi-bundle-mcp-names.js"; -import type { BundleMcpToolRuntime, SessionMcpRuntime } from "./pi-bundle-mcp-types.js"; -import { normalizeToolParameterSchema } from "./pi-tools-parameter-schema.js"; +} from "./agent-bundle-mcp-names.js"; +import type { BundleMcpToolRuntime, SessionMcpRuntime } from "./agent-bundle-mcp-types.js"; +import { normalizeToolParameterSchema } from "./agent-tools-parameter-schema.js"; +import type { AgentToolResult } from "./runtime/index.js"; import type { AnyAgentTool } from "./tools/common.js"; function toAgentToolResult(params: { @@ -158,7 +158,7 @@ export async function createBundleMcpToolRuntime(params: { }) => SessionMcpRuntime; }): Promise { const createRuntime = - params.createRuntime ?? (await import("./pi-bundle-mcp-runtime.js")).createSessionMcpRuntime; + params.createRuntime ?? (await import("./agent-bundle-mcp-runtime.js")).createSessionMcpRuntime; const runtime = createRuntime({ sessionId: `bundle-mcp:${crypto.randomUUID()}`, workspaceDir: params.workspaceDir, diff --git a/src/agents/pi-bundle-mcp-names.test.ts b/src/agents/agent-bundle-mcp-names.test.ts similarity index 95% rename from src/agents/pi-bundle-mcp-names.test.ts rename to src/agents/agent-bundle-mcp-names.test.ts index 0675c8ec2e5..029d5e09877 100644 --- a/src/agents/pi-bundle-mcp-names.test.ts +++ b/src/agents/agent-bundle-mcp-names.test.ts @@ -4,9 +4,9 @@ import { normalizeReservedToolNames, sanitizeServerName, TOOL_NAME_SEPARATOR, -} from "./pi-bundle-mcp-names.js"; +} from "./agent-bundle-mcp-names.js"; -describe("pi bundle MCP names", () => { +describe("agent bundle MCP names", () => { it("sanitizes and disambiguates server names", () => { const usedNames = new Set(); diff --git a/src/agents/pi-bundle-mcp-names.ts b/src/agents/agent-bundle-mcp-names.ts similarity index 100% rename from src/agents/pi-bundle-mcp-names.ts rename to src/agents/agent-bundle-mcp-names.ts diff --git a/src/agents/pi-bundle-mcp-runtime.test.ts b/src/agents/agent-bundle-mcp-runtime.test.ts similarity index 98% rename from src/agents/pi-bundle-mcp-runtime.test.ts rename to src/agents/agent-bundle-mcp-runtime.test.ts index f4c04c4d9e1..1cbd07f0e42 100644 --- a/src/agents/pi-bundle-mcp-runtime.test.ts +++ b/src/agents/agent-bundle-mcp-runtime.test.ts @@ -3,19 +3,21 @@ import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { writeExecutable } from "./bundle-mcp-shared.test-harness.js"; -import { createBundleMcpJsonSchemaValidator } from "./pi-bundle-mcp-runtime.js"; -import { cleanupBundleMcpHarness } from "./pi-bundle-mcp-test-harness.js"; +import { createBundleMcpJsonSchemaValidator } from "./agent-bundle-mcp-runtime.js"; +import { cleanupBundleMcpHarness } from "./agent-bundle-mcp-test-harness.js"; import { testing, getOrCreateSessionMcpRuntime, materializeBundleMcpToolsForRun, retireSessionMcpRuntime, retireSessionMcpRuntimeForSessionKey, -} from "./pi-bundle-mcp-tools.js"; -import type { SessionMcpRuntime } from "./pi-bundle-mcp-types.js"; +} from "./agent-bundle-mcp-tools.js"; +import type { SessionMcpRuntime } from "./agent-bundle-mcp-types.js"; -vi.mock("./embedded-pi-mcp.js", () => ({ - loadEmbeddedPiMcpConfig: (params: { cfg?: { mcp?: { servers?: Record } } }) => ({ +vi.mock("./embedded-agent-mcp.js", () => ({ + loadEmbeddedAgentMcpConfig: (params: { + cfg?: { mcp?: { servers?: Record } }; + }) => ({ diagnostics: [], mcpServers: params.cfg?.mcp?.servers ?? {}, }), diff --git a/src/agents/pi-bundle-mcp-runtime.ts b/src/agents/agent-bundle-mcp-runtime.ts similarity index 98% rename from src/agents/pi-bundle-mcp-runtime.ts rename to src/agents/agent-bundle-mcp-runtime.ts index 8c3256a58d4..927fd8e3661 100644 --- a/src/agents/pi-bundle-mcp-runtime.ts +++ b/src/agents/agent-bundle-mcp-runtime.ts @@ -3,12 +3,12 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv-provider.js"; import type { JsonSchemaType, JsonSchemaValidator, jsonSchemaValidator, } from "@modelcontextprotocol/sdk/validation/types.js"; +import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv-provider.js"; import { Compile } from "typebox/compile"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { logWarn } from "../logger.js"; @@ -19,17 +19,17 @@ import { } from "../shared/json-schema-defaults.js"; import { redactSensitiveUrlLikeString } from "../shared/net/redact-sensitive-url.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; -import { loadEmbeddedPiMcpConfig } from "./embedded-pi-mcp.js"; -import { isMcpConfigRecord } from "./mcp-config-shared.js"; -import { resolveMcpTransport } from "./mcp-transport.js"; -import { sanitizeServerName } from "./pi-bundle-mcp-names.js"; +import { sanitizeServerName } from "./agent-bundle-mcp-names.js"; import type { McpCatalogTool, McpServerCatalog, McpToolCatalog, SessionMcpRuntime, SessionMcpRuntimeManager, -} from "./pi-bundle-mcp-types.js"; +} from "./agent-bundle-mcp-types.js"; +import { loadEmbeddedAgentMcpConfig } from "./embedded-agent-mcp.js"; +import { isMcpConfigRecord } from "./mcp-config-shared.js"; +import { resolveMcpTransport } from "./mcp-transport.js"; type BundleMcpSession = { serverName: string; @@ -39,7 +39,7 @@ type BundleMcpSession = { detachStderr?: () => void; }; -type LoadedMcpConfig = ReturnType; +type LoadedMcpConfig = ReturnType; type ListedTool = Awaited>["tools"][number]; type CreateSessionMcpRuntime = ( params: Parameters[0] & { configFingerprint?: string }, @@ -227,7 +227,7 @@ function loadSessionMcpConfig(params: { loaded: LoadedMcpConfig; fingerprint: string; } { - const loaded = loadEmbeddedPiMcpConfig({ + const loaded = loadEmbeddedAgentMcpConfig({ workspaceDir: params.workspaceDir, cfg: params.cfg, }); diff --git a/src/agents/pi-bundle-mcp-test-harness.ts b/src/agents/agent-bundle-mcp-test-harness.ts similarity index 63% rename from src/agents/pi-bundle-mcp-test-harness.ts rename to src/agents/agent-bundle-mcp-test-harness.ts index b718c3c5f33..b6589f1d904 100644 --- a/src/agents/pi-bundle-mcp-test-harness.ts +++ b/src/agents/agent-bundle-mcp-test-harness.ts @@ -1,4 +1,4 @@ export async function cleanupBundleMcpHarness(): Promise { - const { testing } = await import("./pi-bundle-mcp-tools.js"); + const { testing } = await import("./agent-bundle-mcp-tools.js"); await testing.resetSessionMcpRuntimeManager(); } diff --git a/src/agents/pi-bundle-mcp-tools.materialize.test.ts b/src/agents/agent-bundle-mcp-tools.materialize.test.ts similarity index 96% rename from src/agents/pi-bundle-mcp-tools.materialize.test.ts rename to src/agents/agent-bundle-mcp-tools.materialize.test.ts index 70f145fb7e9..6690d35f5ef 100644 --- a/src/agents/pi-bundle-mcp-tools.materialize.test.ts +++ b/src/agents/agent-bundle-mcp-tools.materialize.test.ts @@ -1,12 +1,12 @@ -import { validateToolArguments } from "@earendil-works/pi-ai"; +import { validateToolArguments } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { getPluginToolMeta } from "../plugins/tools.js"; import { createBundleMcpToolRuntime, materializeBundleMcpToolsForRun, -} from "./pi-bundle-mcp-materialize.js"; -import type { McpCatalogTool } from "./pi-bundle-mcp-types.js"; -import type { SessionMcpRuntime } from "./pi-bundle-mcp-types.js"; +} from "./agent-bundle-mcp-materialize.js"; +import type { McpCatalogTool } from "./agent-bundle-mcp-types.js"; +import type { SessionMcpRuntime } from "./agent-bundle-mcp-types.js"; function expectTextContentBlock(block: unknown, text: string) { const content = block as { type?: string; text?: string } | undefined; diff --git a/src/agents/pi-bundle-mcp-tools.request-boundary.test.ts b/src/agents/agent-bundle-mcp-tools.request-boundary.test.ts similarity index 94% rename from src/agents/pi-bundle-mcp-tools.request-boundary.test.ts rename to src/agents/agent-bundle-mcp-tools.request-boundary.test.ts index 08cf614969f..20f66ee970e 100644 --- a/src/agents/pi-bundle-mcp-tools.request-boundary.test.ts +++ b/src/agents/agent-bundle-mcp-tools.request-boundary.test.ts @@ -3,10 +3,10 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createBundleMcpToolRuntime, materializeBundleMcpToolsForRun, -} from "./pi-bundle-mcp-materialize.js"; -import type { McpCatalogTool, SessionMcpRuntime } from "./pi-bundle-mcp-types.js"; -import { applyFinalEffectiveToolPolicy } from "./pi-embedded-runner/effective-tool-policy.js"; -import { splitSdkTools } from "./pi-embedded-runner/tool-split.js"; +} from "./agent-bundle-mcp-materialize.js"; +import type { McpCatalogTool, SessionMcpRuntime } from "./agent-bundle-mcp-types.js"; +import { applyFinalEffectiveToolPolicy } from "./embedded-agent-runner/effective-tool-policy.js"; +import { splitSdkTools } from "./embedded-agent-runner/tool-split.js"; // Regression coverage for #76063. The reporter's evidence was a captured // outbound provider request body that contained only built-in OpenClaw tools diff --git a/src/agents/pi-bundle-mcp-tools.ts b/src/agents/agent-bundle-mcp-tools.ts similarity index 79% rename from src/agents/pi-bundle-mcp-tools.ts rename to src/agents/agent-bundle-mcp-tools.ts index a45c3011acf..69fa8762041 100644 --- a/src/agents/pi-bundle-mcp-tools.ts +++ b/src/agents/agent-bundle-mcp-tools.ts @@ -5,7 +5,7 @@ export type { McpToolCatalog, SessionMcpRuntime, SessionMcpRuntimeManager, -} from "./pi-bundle-mcp-types.js"; +} from "./agent-bundle-mcp-types.js"; export { testing, testing as __testing, @@ -16,8 +16,8 @@ export { getSessionMcpRuntimeManager, retireSessionMcpRuntime, retireSessionMcpRuntimeForSessionKey, -} from "./pi-bundle-mcp-runtime.js"; +} from "./agent-bundle-mcp-runtime.js"; export { createBundleMcpToolRuntime, materializeBundleMcpToolsForRun, -} from "./pi-bundle-mcp-materialize.js"; +} from "./agent-bundle-mcp-materialize.js"; diff --git a/src/agents/pi-bundle-mcp-types.ts b/src/agents/agent-bundle-mcp-types.ts similarity index 100% rename from src/agents/pi-bundle-mcp-types.ts rename to src/agents/agent-bundle-mcp-types.ts diff --git a/src/agents/agent-command.ts b/src/agents/agent-command.ts index ed22305cb7a..7b05a3cbd2b 100644 --- a/src/agents/agent-command.ts +++ b/src/agents/agent-command.ts @@ -72,6 +72,7 @@ import { resolveAgentRunContext } from "./command/run-context.js"; import { resolveSession } from "./command/session.js"; import type { AgentCommandIngressOpts, AgentCommandOpts } from "./command/types.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js"; +import { classifyEmbeddedAgentRunResultForModelFallback } from "./embedded-agent-runner/result-fallback-classifier.js"; import { resolveFastModeState } from "./fast-mode.js"; import { ensureSelectedAgentHarnessPlugin } from "./harness/runtime-plugin.js"; import { resolveAvailableAgentHarnessPolicy } from "./harness/selection.js"; @@ -95,7 +96,6 @@ import { type ModelVisibilityPolicy, } from "./model-visibility-policy.js"; import { listOpenAIAuthProfileProvidersForAgentRuntime } from "./openai-codex-routing.js"; -import { classifyEmbeddedPiRunResultForModelFallback } from "./pi-embedded-runner/result-fallback-classifier.js"; import { resolveProviderIdForAuth } from "./provider-auth-aliases.js"; import { hydrateResolvedSkillsAsync } from "./skills/snapshot-hydration.js"; import type { SkillSnapshot } from "./skills/types.js"; @@ -1372,7 +1372,7 @@ async function agentCommandInternal( fallbackTrajectoryRecorder?.recordEvent("model.fallback_step", step); }, classifyResult: ({ provider, model, result }) => - classifyEmbeddedPiRunResultForModelFallback({ + classifyEmbeddedAgentRunResultForModelFallback({ provider, model, result, diff --git a/src/agents/pi-compaction-constants.ts b/src/agents/agent-compaction-constants.ts similarity index 100% rename from src/agents/pi-compaction-constants.ts rename to src/agents/agent-compaction-constants.ts diff --git a/src/agents/pi-hooks/compaction-instructions.test.ts b/src/agents/agent-hooks/compaction-instructions.test.ts similarity index 100% rename from src/agents/pi-hooks/compaction-instructions.test.ts rename to src/agents/agent-hooks/compaction-instructions.test.ts diff --git a/src/agents/pi-hooks/compaction-instructions.ts b/src/agents/agent-hooks/compaction-instructions.ts similarity index 100% rename from src/agents/pi-hooks/compaction-instructions.ts rename to src/agents/agent-hooks/compaction-instructions.ts diff --git a/src/agents/pi-hooks/compaction-safeguard-quality.ts b/src/agents/agent-hooks/compaction-safeguard-quality.ts similarity index 100% rename from src/agents/pi-hooks/compaction-safeguard-quality.ts rename to src/agents/agent-hooks/compaction-safeguard-quality.ts diff --git a/src/agents/pi-hooks/compaction-safeguard-runtime.ts b/src/agents/agent-hooks/compaction-safeguard-runtime.ts similarity index 96% rename from src/agents/pi-hooks/compaction-safeguard-runtime.ts rename to src/agents/agent-hooks/compaction-safeguard-runtime.ts index 307bf2ca5ba..d9309b6e73b 100644 --- a/src/agents/pi-hooks/compaction-safeguard-runtime.ts +++ b/src/agents/agent-hooks/compaction-safeguard-runtime.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import type { AgentCompactionIdentifierPolicy } from "../../config/types.agent-defaults.js"; import { createSessionManagerRuntimeRegistry } from "./session-manager-runtime-registry.js"; @@ -13,7 +13,7 @@ export type CompactionSafeguardRuntimeValue = { * Passed through runtime because `ctx.model` is undefined in the compact.ts workflow * (extensionRunner.initialize() is never called in that path). */ - model?: Model; + model?: Model; recentTurnsPreserve?: number; workspaceDir?: string; postCompactionSections?: string[]; diff --git a/src/agents/pi-hooks/compaction-safeguard.test.ts b/src/agents/agent-hooks/compaction-safeguard.test.ts similarity index 99% rename from src/agents/pi-hooks/compaction-safeguard.test.ts rename to src/agents/agent-hooks/compaction-safeguard.test.ts index dedd1c3ca7c..de94ffce59e 100644 --- a/src/agents/pi-hooks/compaction-safeguard.test.ts +++ b/src/agents/agent-hooks/compaction-safeguard.test.ts @@ -1,9 +1,9 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { Api, Model } from "@earendil-works/pi-ai"; -import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import type { ExtensionAPI, ExtensionContext } from "openclaw/plugin-sdk/agent-sessions"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import { @@ -11,7 +11,7 @@ import { registerCompactionProvider, } from "../../plugins/compaction-provider.js"; import * as compactionModule from "../compaction.js"; -import { buildEmbeddedExtensionFactories } from "../pi-embedded-runner/extensions.js"; +import { buildEmbeddedExtensionFactories } from "../embedded-agent-runner/extensions.js"; import { castAgentMessage } from "../test-helpers/agent-message-fixtures.js"; import { consumeCompactionSafeguardCancelReason, @@ -86,7 +86,7 @@ function stubSessionManager(): ExtensionContext["sessionManager"] { return stub; } -function createAnthropicModelFixture(overrides: Partial> = {}): Model { +function createAnthropicModelFixture(overrides: Partial = {}): Model { return { id: "claude-opus-4-5", name: "Claude Opus 4.5", diff --git a/src/agents/pi-hooks/compaction-safeguard.ts b/src/agents/agent-hooks/compaction-safeguard.ts similarity index 99% rename from src/agents/pi-hooks/compaction-safeguard.ts rename to src/agents/agent-hooks/compaction-safeguard.ts index 7e76e55277d..c982bd05a0e 100644 --- a/src/agents/pi-hooks/compaction-safeguard.ts +++ b/src/agents/agent-hooks/compaction-safeguard.ts @@ -1,11 +1,5 @@ import fs from "node:fs"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { - ExtensionAPI, - ExtensionContext, - FileOperations, -} from "@earendil-works/pi-coding-agent"; import { extractSections } from "../../auto-reply/reply/post-compaction-context.js"; import { openRootFile } from "../../infra/boundary-file-read.js"; import { formatErrorMessage } from "../../infra/errors.js"; @@ -35,7 +29,9 @@ import { collectTextContentBlocks } from "../content-blocks.js"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "../copilot-dynamic-headers.js"; import { isTimeoutError } from "../failover-error.js"; import { stripRuntimeContextCustomMessages } from "../internal-runtime-context.js"; +import type { AgentMessage } from "../runtime/index.js"; import { repairToolUseResultPairing } from "../session-transcript-repair.js"; +import type { ExtensionAPI, ExtensionContext, FileOperations } from "../sessions/index.js"; import { extractToolCallsFromAssistant, extractToolResultId } from "../tool-call-id.js"; import { composeSplitTurnInstructions, diff --git a/src/agents/pi-hooks/context-pruning.test.ts b/src/agents/agent-hooks/context-pruning.test.ts similarity index 98% rename from src/agents/pi-hooks/context-pruning.test.ts rename to src/agents/agent-hooks/context-pruning.test.ts index 20981539c4b..0598244076e 100644 --- a/src/agents/pi-hooks/context-pruning.test.ts +++ b/src/agents/agent-hooks/context-pruning.test.ts @@ -1,6 +1,6 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { ToolResultMessage } from "@earendil-works/pi-ai"; -import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import type { ExtensionAPI, ExtensionContext } from "openclaw/plugin-sdk/agent-sessions"; +import type { ToolResultMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { computeEffectiveSettings, diff --git a/src/agents/pi-hooks/context-pruning.ts b/src/agents/agent-hooks/context-pruning.ts similarity index 83% rename from src/agents/pi-hooks/context-pruning.ts rename to src/agents/agent-hooks/context-pruning.ts index 9a504441dcb..325bf1e6ff0 100644 --- a/src/agents/pi-hooks/context-pruning.ts +++ b/src/agents/agent-hooks/context-pruning.ts @@ -1,5 +1,5 @@ /** - * Opt-in context pruning (“microcompact”-style) for Pi sessions. + * Opt-in context pruning (“microcompact”-style) for agent sessions. * * This only affects the in-memory context for the current request; it does not rewrite session * history persisted on disk. diff --git a/src/agents/pi-hooks/context-pruning/extension.ts b/src/agents/agent-hooks/context-pruning/extension.ts similarity index 97% rename from src/agents/pi-hooks/context-pruning/extension.ts rename to src/agents/agent-hooks/context-pruning/extension.ts index 98c031f1bf5..413be87ef79 100644 --- a/src/agents/pi-hooks/context-pruning/extension.ts +++ b/src/agents/agent-hooks/context-pruning/extension.ts @@ -1,4 +1,4 @@ -import type { ContextEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import type { ContextEvent, ExtensionAPI, ExtensionContext } from "../../sessions/index.js"; import { pruneContextMessages } from "./pruner.js"; import { getContextPruningRuntime } from "./runtime.js"; diff --git a/src/agents/pi-hooks/context-pruning/pruner.test.ts b/src/agents/agent-hooks/context-pruning/pruner.test.ts similarity index 98% rename from src/agents/pi-hooks/context-pruning/pruner.test.ts rename to src/agents/agent-hooks/context-pruning/pruner.test.ts index d997ed245b1..897cdc171d0 100644 --- a/src/agents/pi-hooks/context-pruning/pruner.test.ts +++ b/src/agents/agent-hooks/context-pruning/pruner.test.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import type { ExtensionContext } from "openclaw/plugin-sdk/agent-sessions"; import { describe, expect, it } from "vitest"; import { pruneContextMessages } from "./pruner.js"; import { DEFAULT_CONTEXT_PRUNING_SETTINGS } from "./settings.js"; diff --git a/src/agents/pi-hooks/context-pruning/pruner.ts b/src/agents/agent-hooks/context-pruning/pruner.ts similarity index 97% rename from src/agents/pi-hooks/context-pruning/pruner.ts rename to src/agents/agent-hooks/context-pruning/pruner.ts index 121bebc3ef5..a114fb2087d 100644 --- a/src/agents/pi-hooks/context-pruning/pruner.ts +++ b/src/agents/agent-hooks/context-pruning/pruner.ts @@ -1,8 +1,8 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { ImageContent, TextContent, ToolResultMessage } from "@earendil-works/pi-ai"; -import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; +import type { ImageContent, TextContent, ToolResultMessage } from "openclaw/plugin-sdk/llm"; import { CHARS_PER_TOKEN_ESTIMATE, estimateStringChars } from "../../../utils/cjk-chars.js"; -import { dropThinkingBlocks } from "../../pi-embedded-runner/thinking.js"; +import { dropThinkingBlocks } from "../../embedded-agent-runner/thinking.js"; +import type { AgentMessage } from "../../runtime/index.js"; +import type { ExtensionContext } from "../../sessions/index.js"; import type { EffectiveContextPruningSettings } from "./settings.js"; import { makeToolPrunablePredicate } from "./tools.js"; diff --git a/src/agents/pi-hooks/context-pruning/runtime.ts b/src/agents/agent-hooks/context-pruning/runtime.ts similarity index 87% rename from src/agents/pi-hooks/context-pruning/runtime.ts rename to src/agents/agent-hooks/context-pruning/runtime.ts index 22998e853a8..0659c42bc9d 100644 --- a/src/agents/pi-hooks/context-pruning/runtime.ts +++ b/src/agents/agent-hooks/context-pruning/runtime.ts @@ -9,7 +9,7 @@ export type ContextPruningRuntimeValue = { lastCacheTouchAt?: number | null; }; -// Important: this relies on Pi passing the same SessionManager object instance into +// Important: this relies on the embedded agent runtime passing the same SessionManager instance into // ExtensionContext (ctx.sessionManager) that we used when calling setContextPruningRuntime. const registry = createSessionManagerRuntimeRegistry(); diff --git a/src/agents/pi-hooks/context-pruning/settings.ts b/src/agents/agent-hooks/context-pruning/settings.ts similarity index 100% rename from src/agents/pi-hooks/context-pruning/settings.ts rename to src/agents/agent-hooks/context-pruning/settings.ts diff --git a/src/agents/pi-hooks/context-pruning/tools.ts b/src/agents/agent-hooks/context-pruning/tools.ts similarity index 100% rename from src/agents/pi-hooks/context-pruning/tools.ts rename to src/agents/agent-hooks/context-pruning/tools.ts diff --git a/src/agents/pi-hooks/session-manager-runtime-registry.ts b/src/agents/agent-hooks/session-manager-runtime-registry.ts similarity index 100% rename from src/agents/pi-hooks/session-manager-runtime-registry.ts rename to src/agents/agent-hooks/session-manager-runtime-registry.ts diff --git a/src/agents/pi-mcp-style.cache.live.test.ts b/src/agents/agent-mcp-style.cache.live.test.ts similarity index 98% rename from src/agents/pi-mcp-style.cache.live.test.ts rename to src/agents/agent-mcp-style.cache.live.test.ts index 990ab9304d6..030591ec627 100644 --- a/src/agents/pi-mcp-style.cache.live.test.ts +++ b/src/agents/agent-mcp-style.cache.live.test.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage, Tool } from "@earendil-works/pi-ai"; +import type { AssistantMessage, Tool } from "openclaw/plugin-sdk/llm"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { diff --git a/src/agents/pi-model-discovery.auth.test.ts b/src/agents/agent-model-discovery.auth.test.ts similarity index 92% rename from src/agents/pi-model-discovery.auth.test.ts rename to src/agents/agent-model-discovery.auth.test.ts index 302122811c3..fb5da0cd4d9 100644 --- a/src/agents/pi-model-discovery.auth.test.ts +++ b/src/agents/agent-model-discovery.auth.test.ts @@ -2,13 +2,13 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; -import type { AuthProfileStore } from "./auth-profiles.js"; -import { resolvePiCredentialMapFromStore } from "./pi-auth-credentials.js"; +import { resolveAgentCredentialMapFromStore } from "./agent-auth-credentials.js"; import { - addEnvBackedPiCredentials, + addEnvBackedAgentCredentials, scrubLegacyStaticAuthJsonEntriesForDiscovery, -} from "./pi-auth-discovery-core.js"; -import { discoverAuthStorage } from "./pi-model-discovery.js"; +} from "./agent-auth-discovery-core.js"; +import { discoverAuthStorage } from "./agent-model-discovery.js"; +import type { AuthProfileStore } from "./auth-profiles.js"; vi.mock("./model-auth-env-vars.js", () => ({ listProviderEnvAuthLookupKeys: () => ["mistral", "workspace-cloud"], @@ -61,7 +61,7 @@ vi.mock("./model-auth-env.js", () => ({ })); async function createAgentDir(): Promise { - return await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pi-auth-storage-")); + return await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-auth-storage-")); } async function withAgentDir(run: (agentDir: string) => Promise): Promise { @@ -92,8 +92,8 @@ async function readLegacyAuthJson(agentDir: string): Promise { - it("converts runtime auth profiles into pi discovery credentials", () => { - const credentials = resolvePiCredentialMapFromStore({ + it("converts runtime auth profiles into agent discovery credentials", () => { + const credentials = resolveAgentCredentialMapFromStore({ version: 1, profiles: { "openrouter:default": { @@ -132,8 +132,8 @@ describe("discoverAuthStorage", () => { expect(codexCredential?.refresh).toBe("oauth-refresh"); }); - it("keeps keyRef and tokenRef profiles visible only for read-only pi discovery", () => { - const credentials = resolvePiCredentialMapFromStore({ + it("keeps keyRef and tokenRef profiles visible only for read-only agent discovery", () => { + const credentials = resolveAgentCredentialMapFromStore({ version: 1, profiles: { "openrouter:default": { @@ -154,7 +154,7 @@ describe("discoverAuthStorage", () => { }, }, }); - const discoveryCredentials = resolvePiCredentialMapFromStore( + const discoveryCredentials = resolveAgentCredentialMapFromStore( { version: 1, profiles: { @@ -269,7 +269,7 @@ describe("discoverAuthStorage", () => { delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS; try { - const credentials = addEnvBackedPiCredentials({}, { env: process.env }); + const credentials = addEnvBackedAgentCredentials({}, { env: process.env }); expect(credentials.mistral).toEqual({ type: "api_key", @@ -294,8 +294,8 @@ describe("discoverAuthStorage", () => { } }); - it("includes workspace-scoped auth evidence in pi discovery credentials", () => { - const credentials = addEnvBackedPiCredentials( + it("includes workspace-scoped auth evidence in agent discovery credentials", () => { + const credentials = addEnvBackedAgentCredentials( {}, { env: {}, diff --git a/src/agents/agent-model-discovery.internal.test.ts b/src/agents/agent-model-discovery.internal.test.ts new file mode 100644 index 00000000000..8e9ac1ba9bf --- /dev/null +++ b/src/agents/agent-model-discovery.internal.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "vitest"; + +describe("agent-model-discovery internal runtime", () => { + it("loads without the public agent-sessions SDK facade", async () => { + const module = await import("./agent-model-discovery.js"); + expect(typeof module.discoverAuthStorage).toBe("function"); + expect(typeof module.discoverModels).toBe("function"); + expect(typeof module.AuthStorage.inMemory).toBe("function"); + expect(typeof module.ModelRegistry.create).toBe("function"); + }); +}); diff --git a/src/agents/pi-model-discovery.synthetic-auth.test.ts b/src/agents/agent-model-discovery.synthetic-auth.test.ts similarity index 80% rename from src/agents/pi-model-discovery.synthetic-auth.test.ts rename to src/agents/agent-model-discovery.synthetic-auth.test.ts index d38975b6edd..968186eceff 100644 --- a/src/agents/pi-model-discovery.synthetic-auth.test.ts +++ b/src/agents/agent-model-discovery.synthetic-auth.test.ts @@ -34,15 +34,15 @@ vi.mock("./auth-profiles/store.js", () => ({ loadAuthProfileStoreForSecretsRuntime: () => ({ version: 1, profiles: {} }), })); -vi.mock("./pi-auth-discovery-core.js", () => ({ - addEnvBackedPiCredentials: (credentials: Record) => ({ ...credentials }), +vi.mock("./agent-auth-discovery-core.js", () => ({ + addEnvBackedAgentCredentials: (credentials: Record) => ({ ...credentials }), scrubLegacyStaticAuthJsonEntriesForDiscovery: vi.fn(), })); -let resolvePiCredentialsForDiscovery: typeof import("./pi-auth-discovery.js").resolvePiCredentialsForDiscovery; +let resolveAgentCredentialsForDiscovery: typeof import("./agent-auth-discovery.js").resolveAgentCredentialsForDiscovery; async function withAgentDir(run: (agentDir: string) => Promise): Promise { - const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pi-synthetic-auth-")); + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-synthetic-auth-")); try { await run(agentDir); } finally { @@ -50,9 +50,9 @@ async function withAgentDir(run: (agentDir: string) => Promise): Promise { +describe("agent model discovery synthetic auth", () => { beforeAll(async () => { - ({ resolvePiCredentialsForDiscovery } = await import("./pi-auth-discovery.js")); + ({ resolveAgentCredentialsForDiscovery } = await import("./agent-auth-discovery.js")); }); beforeEach(() => { @@ -66,9 +66,9 @@ describe("pi model discovery synthetic auth", () => { vi.unstubAllEnvs(); }); - it("mirrors plugin-owned synthetic cli auth into pi credential discovery", async () => { + it("mirrors plugin-owned synthetic cli auth into credential discovery", async () => { await withAgentDir(async (agentDir) => { - const credentials = resolvePiCredentialsForDiscovery(agentDir, { readOnly: true }); + const credentials = resolveAgentCredentialsForDiscovery(agentDir, { readOnly: true }); expect(resolveRuntimeSyntheticAuthProviderRefs).toHaveBeenCalledTimes(1); expect(resolveRuntimeSyntheticAuthProviderRefs).toHaveBeenCalledWith(); diff --git a/src/agents/pi-model-discovery.test.ts b/src/agents/agent-model-discovery.test.ts similarity index 85% rename from src/agents/pi-model-discovery.test.ts rename to src/agents/agent-model-discovery.test.ts index 7b1c9b2ba3d..36519da7e2d 100644 --- a/src/agents/pi-model-discovery.test.ts +++ b/src/agents/agent-model-discovery.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js"; +import { discoverAuthStorage, discoverModels } from "./agent-model-discovery.js"; function writeModelsJson(agentDir: string, modelId: string): void { fs.writeFileSync( @@ -21,8 +21,8 @@ function writeModelsJson(agentDir: string, modelId: string): void { } describe("discoverModels", () => { - it("clears cached find results when the PI registry refreshes", () => { - const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pi-models-")); + it("clears cached find results when the agent model registry refreshes", () => { + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-agent-models-")); writeModelsJson(agentDir, "old-model"); const authStorage = discoverAuthStorage(agentDir, { skipCredentials: true }); const registry = discoverModels(authStorage, agentDir, { normalizeModels: false }); diff --git a/src/agents/agent-model-discovery.ts b/src/agents/agent-model-discovery.ts new file mode 100644 index 00000000000..942a4aa5fe6 --- /dev/null +++ b/src/agents/agent-model-discovery.ts @@ -0,0 +1,173 @@ +import path from "node:path"; +import type { Model } from "openclaw/plugin-sdk/llm"; +import { normalizeModelCompat } from "../plugins/provider-model-compat.js"; +import { + applyProviderResolvedModelCompatWithPlugins, + applyProviderResolvedTransportWithPlugin, + normalizeProviderResolvedModelWithPlugin, +} from "../plugins/provider-runtime.js"; +import { isRecord } from "../utils.js"; +import { + resolveAgentCredentialsForDiscovery, + scrubLegacyStaticAuthJsonEntriesForDiscovery, + type DiscoverAuthStorageOptions, +} from "./agent-auth-discovery.js"; +import { normalizeProviderId } from "./provider-id.js"; +import { + AuthStorage, + ModelRegistry, + type AuthStorage as AgentAuthStorage, + type ModelRegistry as AgentModelRegistry, +} from "./sessions/index.js"; + +export { AuthStorage, ModelRegistry }; + +type ProviderRuntimeModelLike = Model & { + contextTokens?: number; +}; + +type DiscoveredProviderRuntimeModelLike = Omit & { + api?: string | null; +}; + +type DiscoverModelsOptions = { + providerFilter?: string; + normalizeModels?: boolean; +}; + +export function normalizeDiscoveredAgentModel(value: T, agentDir: string): T { + if (!isRecord(value)) { + return value; + } + if ( + typeof value.id !== "string" || + typeof value.name !== "string" || + typeof value.provider !== "string" + ) { + return value; + } + const model = value as unknown as DiscoveredProviderRuntimeModelLike; + const pluginNormalized = + normalizeProviderResolvedModelWithPlugin({ + provider: model.provider, + context: { + provider: model.provider, + modelId: model.id, + model: model as unknown as ProviderRuntimeModelLike, + agentDir, + }, + }) ?? model; + const compatNormalized = + applyProviderResolvedModelCompatWithPlugins({ + provider: model.provider, + context: { + provider: model.provider, + modelId: model.id, + model: pluginNormalized as unknown as ProviderRuntimeModelLike, + agentDir, + }, + }) ?? pluginNormalized; + const transportNormalized = + applyProviderResolvedTransportWithPlugin({ + provider: model.provider, + context: { + provider: model.provider, + modelId: model.id, + model: compatNormalized as unknown as ProviderRuntimeModelLike, + agentDir, + }, + }) ?? compatNormalized; + if ( + !isRecord(transportNormalized) || + typeof transportNormalized.id !== "string" || + typeof transportNormalized.name !== "string" || + typeof transportNormalized.provider !== "string" || + typeof transportNormalized.api !== "string" + ) { + return value; + } + return normalizeModelCompat(transportNormalized as Model) as T; +} + +function createOpenClawModelRegistry( + authStorage: AgentAuthStorage, + modelsJsonPath: string, + agentDir: string, + options?: DiscoverModelsOptions, +): AgentModelRegistry { + const registry = ModelRegistry.create(authStorage, modelsJsonPath); + const getAll = registry.getAll.bind(registry); + const getAvailable = registry.getAvailable.bind(registry); + const find = registry.find.bind(registry); + const refresh = registry.refresh.bind(registry); + const providerFilter = options?.providerFilter ? normalizeProviderId(options.providerFilter) : ""; + const matchesProviderFilter = (entry: Model) => + !providerFilter || normalizeProviderId(entry.provider) === providerFilter; + const shouldNormalize = options?.normalizeModels !== false; + const findCache = new Map(); + const normalizeEntry = (entry: Model) => + shouldNormalize ? normalizeDiscoveredAgentModel(entry, agentDir) : entry; + + registry.getAll = () => { + const entries = getAll().filter((entry: Model) => matchesProviderFilter(entry)); + return shouldNormalize + ? entries.map((entry: Model) => normalizeDiscoveredAgentModel(entry, agentDir)) + : entries; + }; + registry.getAvailable = () => { + const entries = getAvailable().filter((entry: Model) => matchesProviderFilter(entry)); + return shouldNormalize + ? entries.map((entry: Model) => normalizeDiscoveredAgentModel(entry, agentDir)) + : entries; + }; + registry.find = (provider: string, modelId: string) => { + const normalizedProvider = normalizeProviderId(provider); + const key = `${normalizedProvider}\0${modelId}`; + if (findCache.has(key)) { + return findCache.get(key); + } + const fallbackEntry = find(provider, modelId); + const resolved = fallbackEntry ? normalizeEntry(fallbackEntry) : undefined; + findCache.set(key, resolved); + return resolved; + }; + registry.refresh = () => { + findCache.clear(); + return refresh(); + }; + + return registry; +} + +export function discoverAuthStorage( + agentDir: string, + options?: DiscoverAuthStorageOptions, +): AgentAuthStorage { + const credentials = + options?.skipCredentials === true ? {} : resolveAgentCredentialsForDiscovery(agentDir, options); + const authPath = path.join(agentDir, "auth.json"); + if (options?.readOnly !== true) { + scrubLegacyStaticAuthJsonEntriesForDiscovery(authPath); + } + return AuthStorage.inMemory(credentials); +} + +export function discoverModels( + authStorage: AgentAuthStorage, + agentDir: string, + options?: DiscoverModelsOptions, +): AgentModelRegistry { + return createOpenClawModelRegistry( + authStorage, + path.join(agentDir, "models.json"), + agentDir, + options, + ); +} + +export { + addEnvBackedAgentCredentials, + resolveAgentCredentialsForDiscovery, + scrubLegacyStaticAuthJsonEntriesForDiscovery, + type DiscoverAuthStorageOptions, +} from "./agent-auth-discovery.js"; diff --git a/src/agents/pi-project-settings-snapshot.ts b/src/agents/agent-project-settings-snapshot.ts similarity index 69% rename from src/agents/pi-project-settings-snapshot.ts rename to src/agents/agent-project-settings-snapshot.ts index ba5ca097c91..40aee1907a7 100644 --- a/src/agents/pi-project-settings-snapshot.ts +++ b/src/agents/agent-project-settings-snapshot.ts @@ -1,5 +1,4 @@ import path from "node:path"; -import type { SettingsManager } from "@earendil-works/pi-coding-agent"; import { applyMergePatch } from "../config/merge-patch.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { readRootJsonObjectSync } from "../infra/json-files.js"; @@ -15,30 +14,31 @@ import { loadPluginMetadataSnapshot, type PluginMetadataSnapshot, } from "../plugins/plugin-metadata-snapshot.js"; -import { loadEmbeddedPiMcpConfig } from "./embedded-pi-mcp.js"; +import { loadEmbeddedAgentMcpConfig } from "./embedded-agent-mcp.js"; +import type { SettingsManager } from "./sessions/index.js"; -const log = createSubsystemLogger("embedded-pi-settings"); +const log = createSubsystemLogger("embedded-agent-settings"); -export const DEFAULT_EMBEDDED_PI_PROJECT_SETTINGS_POLICY = "sanitize"; -const SANITIZED_PROJECT_PI_KEYS = ["shellPath", "shellCommandPrefix"] as const; +export const DEFAULT_EMBEDDED_AGENT_PROJECT_SETTINGS_POLICY = "sanitize"; +const SANITIZED_PROJECT_AGENT_KEYS = ["shellPath", "shellCommandPrefix"] as const; -export type EmbeddedPiProjectSettingsPolicy = "trusted" | "sanitize" | "ignore"; +export type EmbeddedAgentProjectSettingsPolicy = "trusted" | "sanitize" | "ignore"; -export type PiSettingsSnapshot = ReturnType & { +export type AgentSettingsSnapshot = ReturnType & { mcpServers?: Record; }; -function sanitizePiSettingsSnapshot(settings: PiSettingsSnapshot): PiSettingsSnapshot { +function sanitizeAgentSettingsSnapshot(settings: AgentSettingsSnapshot): AgentSettingsSnapshot { const sanitized = { ...settings }; // Never allow plugin or workspace-local settings to override shell execution behavior. - for (const key of SANITIZED_PROJECT_PI_KEYS) { + for (const key of SANITIZED_PROJECT_AGENT_KEYS) { delete sanitized[key]; } return sanitized; } -function sanitizeProjectSettings(settings: PiSettingsSnapshot): PiSettingsSnapshot { - return sanitizePiSettingsSnapshot(settings); +function sanitizeProjectSettings(settings: AgentSettingsSnapshot): AgentSettingsSnapshot { + return sanitizeAgentSettingsSnapshot(settings); } function canReuseUnscopedCurrentPluginMetadataSnapshot(config: OpenClawConfig): boolean { @@ -64,7 +64,7 @@ function resolveUnscopedCurrentPluginMetadataSnapshot(params: { function loadBundleSettingsFile(params: { rootDir: string; relativePath: string; -}): PiSettingsSnapshot | null { +}): AgentSettingsSnapshot | null { const absolutePath = path.join(params.rootDir, params.relativePath); const result = readRootJsonObjectSync({ rootDir: params.rootDir, @@ -80,15 +80,15 @@ function loadBundleSettingsFile(params: { log.warn(`${result.error}: ${absolutePath}`); return null; } - return sanitizePiSettingsSnapshot(result.value as PiSettingsSnapshot); + return sanitizeAgentSettingsSnapshot(result.value as AgentSettingsSnapshot); } -export function loadEnabledBundlePiSettingsSnapshot(params: { +export function loadEnabledBundleAgentSettingsSnapshot(params: { cwd: string; cfg?: OpenClawConfig; env?: NodeJS.ProcessEnv; pluginMetadataSnapshot?: PluginMetadataSnapshot; -}): PiSettingsSnapshot { +}): AgentSettingsSnapshot { const workspaceDir = params.cwd.trim(); if (!workspaceDir) { return {}; @@ -129,7 +129,7 @@ export function loadEnabledBundlePiSettingsSnapshot(params: { config.plugins, metadataSnapshot.normalizePluginId, ); - let snapshot: PiSettingsSnapshot = {}; + let snapshot: AgentSettingsSnapshot = {}; for (const record of registry.plugins) { const settingsFiles = record.settingsFiles ?? []; @@ -153,42 +153,42 @@ export function loadEnabledBundlePiSettingsSnapshot(params: { if (!bundleSettings) { continue; } - snapshot = applyMergePatch(snapshot, bundleSettings) as PiSettingsSnapshot; + snapshot = applyMergePatch(snapshot, bundleSettings) as AgentSettingsSnapshot; } } - const embeddedPiMcp = loadEmbeddedPiMcpConfig({ + const embeddedAgentMcp = loadEmbeddedAgentMcpConfig({ workspaceDir, cfg: config, }); - for (const diagnostic of embeddedPiMcp.diagnostics) { + for (const diagnostic of embeddedAgentMcp.diagnostics) { log.warn(`bundle MCP skipped for ${diagnostic.pluginId}: ${diagnostic.message}`); } - if (Object.keys(embeddedPiMcp.mcpServers).length > 0) { + if (Object.keys(embeddedAgentMcp.mcpServers).length > 0) { snapshot = applyMergePatch(snapshot, { - mcpServers: embeddedPiMcp.mcpServers, - }) as PiSettingsSnapshot; + mcpServers: embeddedAgentMcp.mcpServers, + }) as AgentSettingsSnapshot; } return snapshot; } -export function resolveEmbeddedPiProjectSettingsPolicy( +export function resolveEmbeddedAgentProjectSettingsPolicy( cfg?: OpenClawConfig, -): EmbeddedPiProjectSettingsPolicy { - const raw = cfg?.agents?.defaults?.embeddedPi?.projectSettingsPolicy; +): EmbeddedAgentProjectSettingsPolicy { + const raw = cfg?.agents?.defaults?.embeddedAgent?.projectSettingsPolicy; if (raw === "trusted" || raw === "sanitize" || raw === "ignore") { return raw; } - return DEFAULT_EMBEDDED_PI_PROJECT_SETTINGS_POLICY; + return DEFAULT_EMBEDDED_AGENT_PROJECT_SETTINGS_POLICY; } -export function buildEmbeddedPiSettingsSnapshot(params: { - globalSettings: PiSettingsSnapshot; - pluginSettings?: PiSettingsSnapshot; - projectSettings: PiSettingsSnapshot; - policy: EmbeddedPiProjectSettingsPolicy; -}): PiSettingsSnapshot { +export function buildEmbeddedAgentSettingsSnapshot(params: { + globalSettings: AgentSettingsSnapshot; + pluginSettings?: AgentSettingsSnapshot; + projectSettings: AgentSettingsSnapshot; + policy: EmbeddedAgentProjectSettingsPolicy; +}): AgentSettingsSnapshot { const effectiveProjectSettings = params.policy === "ignore" ? {} @@ -197,7 +197,7 @@ export function buildEmbeddedPiSettingsSnapshot(params: { : params.projectSettings; const withPluginSettings = applyMergePatch( params.globalSettings, - sanitizePiSettingsSnapshot(params.pluginSettings ?? {}), - ) as PiSettingsSnapshot; - return applyMergePatch(withPluginSettings, effectiveProjectSettings) as PiSettingsSnapshot; + sanitizeAgentSettingsSnapshot(params.pluginSettings ?? {}), + ) as AgentSettingsSnapshot; + return applyMergePatch(withPluginSettings, effectiveProjectSettings) as AgentSettingsSnapshot; } diff --git a/src/agents/pi-project-settings.bundle.test.ts b/src/agents/agent-project-settings.bundle.test.ts similarity index 93% rename from src/agents/pi-project-settings.bundle.test.ts rename to src/agents/agent-project-settings.bundle.test.ts index 07556f2b4d9..32b04cf9da2 100644 --- a/src/agents/pi-project-settings.bundle.test.ts +++ b/src/agents/agent-project-settings.bundle.test.ts @@ -37,7 +37,7 @@ const bundleTestDeps = await vi.hoisted(async () => { ], }; }; - const loadEmbeddedPiMcpConfig = (params: { + const loadEmbeddedAgentMcpConfig = (params: { workspaceDir: string; cfg?: { mcp?: { servers?: Record } }; }) => { @@ -75,7 +75,7 @@ const bundleTestDeps = await vi.hoisted(async () => { }, }; }; - return { fsSync, loadBundleRegistry, loadEmbeddedPiMcpConfig }; + return { fsSync, loadBundleRegistry, loadEmbeddedAgentMcpConfig }; }); vi.mock("../infra/boundary-file-read.js", () => { @@ -115,11 +115,12 @@ vi.mock("../plugins/plugin-metadata-snapshot.js", () => { }; }); -vi.mock("./embedded-pi-mcp.js", () => ({ - loadEmbeddedPiMcpConfig: bundleTestDeps.loadEmbeddedPiMcpConfig, +vi.mock("./embedded-agent-mcp.js", () => ({ + loadEmbeddedAgentMcpConfig: bundleTestDeps.loadEmbeddedAgentMcpConfig, })); -const { loadEnabledBundlePiSettingsSnapshot } = await import("./pi-project-settings-snapshot.js"); +const { loadEnabledBundleAgentSettingsSnapshot } = + await import("./agent-project-settings-snapshot.js"); const tempDirs = createTrackedTempDirs(); @@ -148,7 +149,7 @@ async function createWorkspaceBundle(params: { return pluginRoot; } -describe("loadEnabledBundlePiSettingsSnapshot", () => { +describe("loadEnabledBundleAgentSettingsSnapshot", () => { it("reuses a compatible plugin metadata snapshot without loading a fresh one", async () => { const workspaceDir = await tempDirs.make("openclaw-workspace-"); const pluginRoot = await createWorkspaceBundle({ workspaceDir }); @@ -162,7 +163,7 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible.mockReturnValueOnce(true); pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); - const snapshot = loadEnabledBundlePiSettingsSnapshot({ + const snapshot = loadEnabledBundleAgentSettingsSnapshot({ cwd: workspaceDir, cfg: { plugins: { @@ -187,7 +188,7 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { }, normalizePluginId: (id: string) => id.trim(), } as unknown as Parameters< - typeof loadEnabledBundlePiSettingsSnapshot + typeof loadEnabledBundleAgentSettingsSnapshot >[0]["pluginMetadataSnapshot"], }); @@ -208,7 +209,7 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { pluginMetadataSnapshotMocks.isPluginMetadataSnapshotCompatible.mockReturnValueOnce(false); pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); - const snapshot = loadEnabledBundlePiSettingsSnapshot({ + const snapshot = loadEnabledBundleAgentSettingsSnapshot({ cwd: workspaceDir, cfg: { plugins: { @@ -221,7 +222,7 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { manifestRegistry: { diagnostics: [], plugins: [] }, normalizePluginId: (id: string) => id.trim(), } as unknown as Parameters< - typeof loadEnabledBundlePiSettingsSnapshot + typeof loadEnabledBundleAgentSettingsSnapshot >[0]["pluginMetadataSnapshot"], }); @@ -258,7 +259,7 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { }); pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); - const snapshot = loadEnabledBundlePiSettingsSnapshot({ + const snapshot = loadEnabledBundleAgentSettingsSnapshot({ cwd: workspaceDir, cfg: { plugins: { @@ -285,7 +286,7 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { pluginMetadataSnapshotMocks.getCurrentPluginMetadataSnapshot.mockReturnValueOnce(undefined); pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); - const snapshot = loadEnabledBundlePiSettingsSnapshot({ + const snapshot = loadEnabledBundleAgentSettingsSnapshot({ cwd: workspaceDir, cfg: { plugins: { @@ -345,7 +346,7 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { ); pluginMetadataSnapshotMocks.loadPluginMetadataSnapshot.mockClear(); - const snapshot = loadEnabledBundlePiSettingsSnapshot({ + const snapshot = loadEnabledBundleAgentSettingsSnapshot({ cwd: workspaceDir, cfg: { plugins: { @@ -399,7 +400,7 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { "utf-8", ); - const snapshot = loadEnabledBundlePiSettingsSnapshot({ + const snapshot = loadEnabledBundleAgentSettingsSnapshot({ cwd: workspaceDir, cfg: { plugins: { @@ -426,7 +427,7 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { }, }); - const overridden = loadEnabledBundlePiSettingsSnapshot({ + const overridden = loadEnabledBundleAgentSettingsSnapshot({ cwd: workspaceDir, cfg: { mcp: { @@ -465,7 +466,7 @@ describe("loadEnabledBundlePiSettingsSnapshot", () => { "utf-8", ); - const snapshot = loadEnabledBundlePiSettingsSnapshot({ + const snapshot = loadEnabledBundleAgentSettingsSnapshot({ cwd: workspaceDir, cfg: { plugins: { diff --git a/src/agents/pi-project-settings.test.ts b/src/agents/agent-project-settings.test.ts similarity index 68% rename from src/agents/pi-project-settings.test.ts rename to src/agents/agent-project-settings.test.ts index f763512249a..b81559ee607 100644 --- a/src/agents/pi-project-settings.test.ts +++ b/src/agents/agent-project-settings.test.ts @@ -3,36 +3,48 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { - buildEmbeddedPiSettingsSnapshot, - DEFAULT_EMBEDDED_PI_PROJECT_SETTINGS_POLICY, - resolveEmbeddedPiProjectSettingsPolicy, -} from "./pi-project-settings-snapshot.js"; -import { createPreparedEmbeddedPiSettingsManager } from "./pi-project-settings.js"; + buildEmbeddedAgentSettingsSnapshot, + DEFAULT_EMBEDDED_AGENT_PROJECT_SETTINGS_POLICY, + resolveEmbeddedAgentProjectSettingsPolicy, +} from "./agent-project-settings-snapshot.js"; +import { createPreparedEmbeddedAgentSettingsManager } from "./agent-project-settings.js"; -type EmbeddedPiSettingsArgs = Parameters[0]; +type EmbeddedAgentSettingsArgs = Parameters[0]; -describe("resolveEmbeddedPiProjectSettingsPolicy", () => { +describe("resolveEmbeddedAgentProjectSettingsPolicy", () => { it("defaults to sanitize", () => { - expect(resolveEmbeddedPiProjectSettingsPolicy()).toBe( - DEFAULT_EMBEDDED_PI_PROJECT_SETTINGS_POLICY, + expect(resolveEmbeddedAgentProjectSettingsPolicy()).toBe( + DEFAULT_EMBEDDED_AGENT_PROJECT_SETTINGS_POLICY, ); }); it("accepts trusted and ignore modes", () => { expect( - resolveEmbeddedPiProjectSettingsPolicy({ - agents: { defaults: { embeddedPi: { projectSettingsPolicy: "trusted" } } }, + resolveEmbeddedAgentProjectSettingsPolicy({ + agents: { defaults: { embeddedAgent: { projectSettingsPolicy: "trusted" } } }, }), ).toBe("trusted"); expect( - resolveEmbeddedPiProjectSettingsPolicy({ - agents: { defaults: { embeddedPi: { projectSettingsPolicy: "ignore" } } }, + resolveEmbeddedAgentProjectSettingsPolicy({ + agents: { defaults: { embeddedAgent: { projectSettingsPolicy: "ignore" } } }, + }), + ).toBe("ignore"); + }); + + it("uses embeddedAgent as the only runtime config key", () => { + expect( + resolveEmbeddedAgentProjectSettingsPolicy({ + agents: { + defaults: { + embeddedAgent: { projectSettingsPolicy: "ignore" }, + }, + }, }), ).toBe("ignore"); }); }); -describe("buildEmbeddedPiSettingsSnapshot", () => { +describe("buildEmbeddedAgentSettingsSnapshot", () => { const globalSettings = { shellPath: "/bin/zsh", compaction: { reserveTokens: 20_000, keepRecentTokens: 20_000 }, @@ -45,7 +57,7 @@ describe("buildEmbeddedPiSettingsSnapshot", () => { }; it("sanitize mode strips shell path + prefix but keeps other project settings", () => { - const snapshot = buildEmbeddedPiSettingsSnapshot({ + const snapshot = buildEmbeddedAgentSettingsSnapshot({ globalSettings, pluginSettings: {}, projectSettings, @@ -58,7 +70,7 @@ describe("buildEmbeddedPiSettingsSnapshot", () => { }); it("ignore mode drops all project settings", () => { - const snapshot = buildEmbeddedPiSettingsSnapshot({ + const snapshot = buildEmbeddedAgentSettingsSnapshot({ globalSettings, pluginSettings: {}, projectSettings, @@ -71,7 +83,7 @@ describe("buildEmbeddedPiSettingsSnapshot", () => { }); it("trusted mode keeps project settings as-is", () => { - const snapshot = buildEmbeddedPiSettingsSnapshot({ + const snapshot = buildEmbeddedAgentSettingsSnapshot({ globalSettings, pluginSettings: {}, projectSettings, @@ -84,7 +96,7 @@ describe("buildEmbeddedPiSettingsSnapshot", () => { }); it("applies sanitized plugin settings before project settings", () => { - const snapshot = buildEmbeddedPiSettingsSnapshot({ + const snapshot = buildEmbeddedAgentSettingsSnapshot({ globalSettings, pluginSettings: { shellPath: "/tmp/blocked-shell", @@ -100,8 +112,8 @@ describe("buildEmbeddedPiSettingsSnapshot", () => { expect(snapshot.hideThinkingBlock).toBe(true); }); - it("lets project Pi settings override bundle MCP defaults", () => { - const snapshot = buildEmbeddedPiSettingsSnapshot({ + it("lets project embedded-agent settings override bundle MCP defaults", () => { + const snapshot = buildEmbeddedAgentSettingsSnapshot({ globalSettings, pluginSettings: { mcpServers: { @@ -110,7 +122,7 @@ describe("buildEmbeddedPiSettingsSnapshot", () => { args: ["/plugins/probe.mjs"], }, }, - } as EmbeddedPiSettingsArgs["pluginSettings"], + } as EmbeddedAgentSettingsArgs["pluginSettings"], projectSettings: { mcpServers: { bundleProbe: { @@ -118,7 +130,7 @@ describe("buildEmbeddedPiSettingsSnapshot", () => { args: ["/workspace/probe.ts"], }, }, - } as EmbeddedPiSettingsArgs["projectSettings"], + } as EmbeddedAgentSettingsArgs["projectSettings"], policy: "sanitize", }); @@ -131,13 +143,13 @@ describe("buildEmbeddedPiSettingsSnapshot", () => { }); }); -describe("createPreparedEmbeddedPiSettingsManager", () => { +describe("createPreparedEmbeddedAgentSettingsManager", () => { it("keeps trusted file-backed settings runtime-scoped after preparation", async () => { - const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pi-settings-")); + const baseDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-settings-")); try { const cwd = path.join(baseDir, "workspace"); const agentDir = path.join(baseDir, "agent"); - const projectSettingsDir = path.join(cwd, ".pi"); + const projectSettingsDir = path.join(cwd, ".openclaw"); const agentSettingsPath = path.join(agentDir, "settings.json"); await fs.mkdir(projectSettingsDir, { recursive: true }); await fs.mkdir(agentDir, { recursive: true }); @@ -152,11 +164,11 @@ describe("createPreparedEmbeddedPiSettingsManager", () => { "utf8", ); - const settingsManager = createPreparedEmbeddedPiSettingsManager({ + const settingsManager = createPreparedEmbeddedAgentSettingsManager({ cwd, agentDir, cfg: { - agents: { defaults: { embeddedPi: { projectSettingsPolicy: "trusted" } } }, + agents: { defaults: { embeddedAgent: { projectSettingsPolicy: "trusted" } } }, }, }); diff --git a/src/agents/pi-project-settings.ts b/src/agents/agent-project-settings.ts similarity index 63% rename from src/agents/pi-project-settings.ts rename to src/agents/agent-project-settings.ts index 46cc1462838..1062cd0f345 100644 --- a/src/agents/pi-project-settings.ts +++ b/src/agents/agent-project-settings.ts @@ -1,22 +1,22 @@ -import { SettingsManager } from "@earendil-works/pi-coding-agent"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js"; import { - buildEmbeddedPiSettingsSnapshot, - loadEnabledBundlePiSettingsSnapshot, - resolveEmbeddedPiProjectSettingsPolicy, -} from "./pi-project-settings-snapshot.js"; -import { applyPiCompactionSettingsFromConfig } from "./pi-settings.js"; + buildEmbeddedAgentSettingsSnapshot, + loadEnabledBundleAgentSettingsSnapshot, + resolveEmbeddedAgentProjectSettingsPolicy, +} from "./agent-project-settings-snapshot.js"; +import { applyAgentCompactionSettingsFromConfig } from "./agent-settings.js"; +import { SettingsManager } from "./sessions/index.js"; -function createEmbeddedPiSettingsManager(params: { +function createEmbeddedAgentSettingsManager(params: { cwd: string; agentDir: string; cfg?: OpenClawConfig; pluginMetadataSnapshot?: PluginMetadataSnapshot; }): SettingsManager { const fileSettingsManager = SettingsManager.create(params.cwd, params.agentDir); - const policy = resolveEmbeddedPiProjectSettingsPolicy(params.cfg); - const pluginSettings = loadEnabledBundlePiSettingsSnapshot({ + const policy = resolveEmbeddedAgentProjectSettingsPolicy(params.cfg); + const pluginSettings = loadEnabledBundleAgentSettingsSnapshot({ cwd: params.cwd, cfg: params.cfg, pluginMetadataSnapshot: params.pluginMetadataSnapshot, @@ -25,7 +25,7 @@ function createEmbeddedPiSettingsManager(params: { if (policy === "trusted" && !hasPluginSettings) { return fileSettingsManager; } - const settings = buildEmbeddedPiSettingsSnapshot({ + const settings = buildEmbeddedAgentSettingsSnapshot({ globalSettings: fileSettingsManager.getGlobalSettings(), pluginSettings, projectSettings: fileSettingsManager.getProjectSettings(), @@ -34,9 +34,11 @@ function createEmbeddedPiSettingsManager(params: { return SettingsManager.inMemory(settings); } -function createRuntimeEmbeddedPiSettingsManager(settingsManager: SettingsManager): SettingsManager { +function createRuntimeEmbeddedAgentSettingsManager( + settingsManager: SettingsManager, +): SettingsManager { return SettingsManager.inMemory( - buildEmbeddedPiSettingsSnapshot({ + buildEmbeddedAgentSettingsSnapshot({ globalSettings: settingsManager.getGlobalSettings(), pluginSettings: {}, projectSettings: settingsManager.getProjectSettings(), @@ -45,7 +47,7 @@ function createRuntimeEmbeddedPiSettingsManager(settingsManager: SettingsManager ); } -export function createPreparedEmbeddedPiSettingsManager(params: { +export function createPreparedEmbeddedAgentSettingsManager(params: { cwd: string; agentDir: string; cfg?: OpenClawConfig; @@ -53,15 +55,15 @@ export function createPreparedEmbeddedPiSettingsManager(params: { /** Resolved context window budget so reserve-token floor can be capped for small models. */ contextTokenBudget?: number; }): SettingsManager { - const settingsManager = createRuntimeEmbeddedPiSettingsManager( - createEmbeddedPiSettingsManager(params), + const settingsManager = createRuntimeEmbeddedAgentSettingsManager( + createEmbeddedAgentSettingsManager(params), ); - applyPiCompactionSettingsFromConfig({ + applyAgentCompactionSettingsFromConfig({ settingsManager, cfg: params.cfg, contextTokenBudget: params.contextTokenBudget, }); - // Disable the pi-coding-agent auto-retry. OpenClaw has its own comprehensive + // Disable the session runtime auto-retry. OpenClaw has its own comprehensive // retry layer (failover rotation, auth profile rotation, empty-error retry, // thinking-level fallback) in run.ts. Having both layers active creates a // double-retry that can replay failed tool calls in an unbounded loop (#73781). diff --git a/src/agents/agent-runtime-id.ts b/src/agents/agent-runtime-id.ts new file mode 100644 index 00000000000..55a95eee1c0 --- /dev/null +++ b/src/agents/agent-runtime-id.ts @@ -0,0 +1,42 @@ +export type EmbeddedAgentRuntime = "openclaw" | "auto" | (string & {}); + +export const OPENCLAW_AGENT_RUNTIME_ID = "openclaw"; +export const AUTO_AGENT_RUNTIME_ID = "auto"; + +export function normalizeEmbeddedAgentRuntime(raw: string | undefined): EmbeddedAgentRuntime { + const value = raw?.trim(); + if (!value) { + return OPENCLAW_AGENT_RUNTIME_ID; + } + if (value === "openclaw") { + return OPENCLAW_AGENT_RUNTIME_ID; + } + if (value === "pi") { + return OPENCLAW_AGENT_RUNTIME_ID; + } + if (value === "auto") { + return AUTO_AGENT_RUNTIME_ID; + } + if (value === "codex-app-server") { + return "codex"; + } + return value; +} + +export function normalizeOptionalAgentRuntimeId(raw: unknown): EmbeddedAgentRuntime | undefined { + if (typeof raw !== "string") { + return undefined; + } + const value = raw.trim().toLowerCase(); + return value ? normalizeEmbeddedAgentRuntime(value) : undefined; +} + +export function resolveEmbeddedAgentRuntime( + env: NodeJS.ProcessEnv = process.env, +): EmbeddedAgentRuntime { + return normalizeOptionalAgentRuntimeId(env.OPENCLAW_AGENT_RUNTIME) ?? OPENCLAW_AGENT_RUNTIME_ID; +} + +export function isDefaultAgentRuntimeId(runtime: string | undefined): boolean { + return runtime === undefined || runtime === AUTO_AGENT_RUNTIME_ID || runtime === "default"; +} diff --git a/src/agents/agent-scope-config.ts b/src/agents/agent-scope-config.ts index 912e07c0d67..d8c9863a99b 100644 --- a/src/agents/agent-scope-config.ts +++ b/src/agents/agent-scope-config.ts @@ -36,7 +36,7 @@ export type ResolvedAgentConfig = { groupChat?: AgentEntry["groupChat"]; subagents?: AgentEntry["subagents"]; runRetries?: AgentEntry["runRetries"]; - embeddedPi?: AgentEntry["embeddedPi"]; + embeddedAgent?: AgentEntry["embeddedAgent"]; sandbox?: AgentEntry["sandbox"]; tools?: AgentEntry["tools"]; }; @@ -149,8 +149,10 @@ export function resolveAgentConfig( typeof entry.runRetries === "object" && entry.runRetries ? { ...agentDefaults?.runRetries, ...entry.runRetries } : agentDefaults?.runRetries, - embeddedPi: - typeof entry.embeddedPi === "object" && entry.embeddedPi ? entry.embeddedPi : undefined, + embeddedAgent: + typeof entry.embeddedAgent === "object" && entry.embeddedAgent + ? entry.embeddedAgent + : undefined, sandbox: entry.sandbox, tools: entry.tools, }; diff --git a/src/agents/agent-scope.ts b/src/agents/agent-scope.ts index 386d17ba975..a2e6df5c350 100644 --- a/src/agents/agent-scope.ts +++ b/src/agents/agent-scope.ts @@ -306,12 +306,13 @@ export function resolveSessionAgentId(params: { export function resolveAgentExecutionContract( cfg: OpenClawConfig | undefined, agentId?: string | null, -): NonNullable["executionContract"]> | undefined { - const defaultContract = cfg?.agents?.defaults?.embeddedPi?.executionContract; +): NonNullable["executionContract"]> | undefined { + const defaultContract = cfg?.agents?.defaults?.embeddedAgent?.executionContract; if (!cfg || !agentId) { return defaultContract; } - const agentContract = resolveAgentConfig(cfg, agentId)?.embeddedPi?.executionContract; + const agentConfig = resolveAgentConfig(cfg, agentId); + const agentContract = agentConfig?.embeddedAgent?.executionContract; return agentContract ?? defaultContract; } diff --git a/src/agents/pi-settings.test.ts b/src/agents/agent-settings.test.ts similarity index 81% rename from src/agents/pi-settings.test.ts rename to src/agents/agent-settings.test.ts index bca472c9b14..09d52b363cc 100644 --- a/src/agents/pi-settings.test.ts +++ b/src/agents/agent-settings.test.ts @@ -1,16 +1,16 @@ import { describe, expect, it, vi } from "vitest"; -import { MIN_PROMPT_BUDGET_RATIO, MIN_PROMPT_BUDGET_TOKENS } from "./pi-compaction-constants.js"; +import { MIN_PROMPT_BUDGET_RATIO, MIN_PROMPT_BUDGET_TOKENS } from "./agent-compaction-constants.js"; import { - applyPiAutoCompactionGuard, - applyPiCompactionSettingsFromConfig, - DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR, + applyAgentAutoCompactionGuard, + applyAgentCompactionSettingsFromConfig, + DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR, isSilentOverflowProneModel, resolveEffectiveCompactionMode, resolveCompactionReserveTokensFloor, - shouldDisablePiAutoCompaction, -} from "./pi-settings.js"; + shouldDisableAgentAutoCompaction, +} from "./agent-settings.js"; -describe("applyPiCompactionSettingsFromConfig", () => { +describe("applyAgentCompactionSettingsFromConfig", () => { it("bumps reserveTokens when below floor", () => { const settingsManager = { getCompactionReserveTokens: () => 16_384, @@ -18,12 +18,12 @@ describe("applyPiCompactionSettingsFromConfig", () => { applyOverrides: vi.fn(), }; - const result = applyPiCompactionSettingsFromConfig({ settingsManager }); + const result = applyAgentCompactionSettingsFromConfig({ settingsManager }); expect(result.didOverride).toBe(true); - expect(result.compaction.reserveTokens).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR); + expect(result.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR); expect(settingsManager.applyOverrides).toHaveBeenCalledWith({ - compaction: { reserveTokens: DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR }, + compaction: { reserveTokens: DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR }, }); }); @@ -31,7 +31,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { const cfg = { agents: { defaults: { - compaction: { reserveTokensFloor: DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR }, + compaction: { reserveTokensFloor: DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR }, }, }, } as const; @@ -47,21 +47,21 @@ describe("applyPiCompactionSettingsFromConfig", () => { }), }; - const first = applyPiCompactionSettingsFromConfig({ + const first = applyAgentCompactionSettingsFromConfig({ settingsManager, cfg, contextTokenBudget: 100_000, }); - expect(first.compaction.reserveTokens).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR); + expect(first.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR); reserve = 16_384; - const second = applyPiCompactionSettingsFromConfig({ + const second = applyAgentCompactionSettingsFromConfig({ settingsManager, cfg, contextTokenBudget: 100_000, }); - expect(second.compaction.reserveTokens).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR); - expect(reserve).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR); + expect(second.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR); + expect(reserve).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR); }); it("does not override when already above floor and not in safeguard mode", () => { @@ -71,7 +71,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { applyOverrides: vi.fn(), }; - const result = applyPiCompactionSettingsFromConfig({ + const result = applyAgentCompactionSettingsFromConfig({ settingsManager, cfg: { agents: { defaults: { compaction: { mode: "default" } } } }, }); @@ -88,7 +88,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { applyOverrides: vi.fn(), }; - const result = applyPiCompactionSettingsFromConfig({ + const result = applyAgentCompactionSettingsFromConfig({ settingsManager, cfg: { agents: { @@ -112,7 +112,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { applyOverrides: vi.fn(), }; - const result = applyPiCompactionSettingsFromConfig({ + const result = applyAgentCompactionSettingsFromConfig({ settingsManager, cfg: { agents: { @@ -138,7 +138,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { applyOverrides: vi.fn(), }; - const result = applyPiCompactionSettingsFromConfig({ + const result = applyAgentCompactionSettingsFromConfig({ settingsManager, cfg: { agents: { defaults: { compaction: { mode: "safeguard" } } } }, }); @@ -154,7 +154,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { applyOverrides: vi.fn(), }; - const result = applyPiCompactionSettingsFromConfig({ + const result = applyAgentCompactionSettingsFromConfig({ settingsManager, cfg: { agents: { defaults: { compaction: { mode: "safeguard", keepRecentTokens: 0 } } } }, }); @@ -164,7 +164,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { }); it("caps floor to context window ratio for small-context models", () => { - // Pi SDK default reserveTokens is 16 384. With a 16 384 context window + // Embedded runner default reserveTokens is 16 384. With a 16 384 context window // the default floor (20 000) exceeds the window. The aligned cap // computes: minPromptBudget = min(8_000, floor(16_384 * 0.5)) = 8_000, // maxReserve = 16_384 - 8_000 = 8_384. Since current (16_384) > capped @@ -175,7 +175,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { applyOverrides: vi.fn(), }; - const result = applyPiCompactionSettingsFromConfig({ + const result = applyAgentCompactionSettingsFromConfig({ settingsManager, contextTokenBudget: 16_384, }); @@ -184,7 +184,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { // With the cap, it stays at 16_384 (the current value). expect(result.compaction.reserveTokens).toBe(16_384); expect(result.compaction.reserveTokens).toBeLessThan( - DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR, + DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR, ); expect(result.didOverride).toBe(false); expect(settingsManager.applyOverrides).not.toHaveBeenCalled(); @@ -200,7 +200,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { // User sets reserveTokens=2048 but NOT reserveTokensFloor (default 20_000 applies). // Pre-fix: target = max(2048, 20_000) = 20_000 → exceeds 16_384 context → infinite loop. // Post-fix: floor capped to 8_384 → target = max(2048, 8_384) = 8_384 → works. - const result = applyPiCompactionSettingsFromConfig({ + const result = applyAgentCompactionSettingsFromConfig({ settingsManager, cfg: { agents: { @@ -220,7 +220,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { }); it("applies capped floor when current reserve is below it on small-context models", () => { - // Simulate a Pi SDK default of 4 096 with a 16 384 context window. + // Simulate an embedded runner default of 4 096 with a 16 384 context window. // minPromptBudget = min(8_000, floor(16_384 * 0.5)) = 8_000. // maxReserve = 16_384 - 8_000 = 8_384. // Capped floor = min(20_000, 8_384) = 8_384. @@ -231,7 +231,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { applyOverrides: vi.fn(), }; - const result = applyPiCompactionSettingsFromConfig({ + const result = applyAgentCompactionSettingsFromConfig({ settingsManager, contextTokenBudget: 16_384, }); @@ -258,7 +258,7 @@ describe("applyPiCompactionSettingsFromConfig", () => { // User explicitly sets reserveTokens=2048 and reserveTokensFloor=0. // With contextTokenBudget=16384, the capped floor = min(0, 8192) = 0. // targetReserveTokens = max(2048, 0) = 2048. - const result = applyPiCompactionSettingsFromConfig({ + const result = applyAgentCompactionSettingsFromConfig({ settingsManager, cfg: { agents: { @@ -286,14 +286,14 @@ describe("applyPiCompactionSettingsFromConfig", () => { // 32 768 context window → minPromptBudget = min(8_000, floor(32_768 * 0.5)) = 8_000. // maxReserve = 32_768 - 8_000 = 24_768. // Since 24_768 > 20_000 (DEFAULT_FLOOR), the floor is NOT capped and stays at 20_000. - const result = applyPiCompactionSettingsFromConfig({ + const result = applyAgentCompactionSettingsFromConfig({ settingsManager, contextTokenBudget: 32_768, }); - expect(result.compaction.reserveTokens).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR); + expect(result.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR); expect(settingsManager.applyOverrides).toHaveBeenCalledWith({ - compaction: { reserveTokens: DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR }, + compaction: { reserveTokens: DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR }, }); }); @@ -306,14 +306,14 @@ describe("applyPiCompactionSettingsFromConfig", () => { // 200 000 context window → maxReserve = 200_000 - 8_000 = 192_000. // floor (20 000) is well within that cap. - const result = applyPiCompactionSettingsFromConfig({ + const result = applyAgentCompactionSettingsFromConfig({ settingsManager, contextTokenBudget: 200_000, }); - expect(result.compaction.reserveTokens).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR); + expect(result.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR); expect(settingsManager.applyOverrides).toHaveBeenCalledWith({ - compaction: { reserveTokens: DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR }, + compaction: { reserveTokens: DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR }, }); }); @@ -325,15 +325,17 @@ describe("applyPiCompactionSettingsFromConfig", () => { }; // No contextTokenBudget → backward-compatible behavior, floor = 20 000. - const result = applyPiCompactionSettingsFromConfig({ settingsManager }); + const result = applyAgentCompactionSettingsFromConfig({ settingsManager }); - expect(result.compaction.reserveTokens).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR); + expect(result.compaction.reserveTokens).toBe(DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR); }); }); describe("resolveCompactionReserveTokensFloor", () => { it("returns the default when config is missing", () => { - expect(resolveCompactionReserveTokensFloor()).toBe(DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR); + expect(resolveCompactionReserveTokensFloor()).toBe( + DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR, + ); }); it("accepts configured floors, including zero", () => { @@ -417,7 +419,7 @@ describe("isSilentOverflowProneModel", () => { // family name so direct gateway deployments hit the guard regardless of // what `provider` field the user picked — gateways relabel the upstream // identity, so `provider` here can be anything from `openai` to a custom - // string. False positives only disable Pi's secondary compaction path; + // string. False positives only disable OpenClaw runtime's secondary compaction path; // OpenClaw's preemptive compaction continues to handle real overflow. it("flags bare glm- model ids without a namespace prefix, regardless of provider", () => { expect(isSilentOverflowProneModel({ provider: "custom", modelId: "glm-5.1" })).toBe(true); @@ -429,7 +431,7 @@ describe("isSilentOverflowProneModel", () => { // Detection is intentionally narrow to z.ai-style accounting. Namespaced GLM // ids that route through providers with their own overflow accounting must // NOT be flagged — those hosts may not exhibit the z.ai silent-overflow - // shape, and disabling Pi auto-compaction for them would over-broaden the + // shape, and disabling embedded auto-compaction for them would over-broaden the // kill surface beyond the reproducible repro. it("does not flag namespaced GLM ids routed through non-z.ai hosts", () => { expect( @@ -440,7 +442,7 @@ describe("isSilentOverflowProneModel", () => { ).toBe(false); }); - // pi-ai's overflow.ts only documents z.ai as the silent-overflow style. We + // shared model runtime's overflow.ts only documents z.ai as the silent-overflow style. We // intentionally do NOT extend the guard to anthropic/openai/google/openrouter- // anthropic routes — adding them without a reproducible repro would broaden // the kill surface and regress baseline behavior for those providers. @@ -468,12 +470,12 @@ describe("isSilentOverflowProneModel", () => { }); }); -describe("shouldDisablePiAutoCompaction", () => { +describe("shouldDisableAgentAutoCompaction", () => { it("returns false with no owner, default mode, and ordinary provider behavior", () => { - expect(shouldDisablePiAutoCompaction({})).toBe(false); - expect(shouldDisablePiAutoCompaction({ compactionMode: "default" })).toBe(false); + expect(shouldDisableAgentAutoCompaction({})).toBe(false); + expect(shouldDisableAgentAutoCompaction({ compactionMode: "default" })).toBe(false); expect( - shouldDisablePiAutoCompaction({ + shouldDisableAgentAutoCompaction({ contextEngineInfo: { id: "legacy", name: "Legacy", ownsCompaction: false }, compactionMode: "default", silentOverflowProneProvider: false, @@ -483,30 +485,30 @@ describe("shouldDisablePiAutoCompaction", () => { it("returns true when a context engine owns compaction", () => { expect( - shouldDisablePiAutoCompaction({ + shouldDisableAgentAutoCompaction({ contextEngineInfo: { id: "third-party", name: "Third-party", ownsCompaction: true }, }), ).toBe(true); }); it("returns true when effective compaction mode is safeguard", () => { - expect(shouldDisablePiAutoCompaction({ compactionMode: "safeguard" })).toBe(true); + expect(shouldDisableAgentAutoCompaction({ compactionMode: "safeguard" })).toBe(true); }); it("returns true for silent-overflow-prone providers", () => { - expect(shouldDisablePiAutoCompaction({ silentOverflowProneProvider: true })).toBe(true); + expect(shouldDisableAgentAutoCompaction({ silentOverflowProneProvider: true })).toBe(true); }); }); -describe("applyPiAutoCompactionGuard", () => { - // Direct repro of openclaw#75799: pi-ai's silent-overflow detection misfires - // on a successful turn against z.ai-style providers, triggering Pi's +describe("applyAgentAutoCompactionGuard", () => { + // Direct repro of openclaw#75799: shared model runtime's silent-overflow detection misfires + // on a successful turn against z.ai-style providers, triggering OpenClaw runtime's // _runAutoCompaction from inside Session.prompt() and reassigning // agent.state.messages between the runner's prompt.submitted trajectory - // event and the provider request. Disabling Pi auto-compaction here keeps + // event and the provider request. Disabling embedded auto-compaction here keeps // state.messages intact; OpenClaw's preemptive compaction continues to // handle real overflow on its own path. - it("disables Pi auto-compaction for silent-overflow-prone providers", () => { + it("disables embedded auto-compaction for silent-overflow-prone providers", () => { const setCompactionEnabled = vi.fn(); const settingsManager = { getCompactionReserveTokens: () => 20_000, @@ -515,7 +517,7 @@ describe("applyPiAutoCompactionGuard", () => { setCompactionEnabled, }; - const result = applyPiAutoCompactionGuard({ + const result = applyAgentAutoCompactionGuard({ settingsManager, silentOverflowProneProvider: true, }); @@ -524,7 +526,7 @@ describe("applyPiAutoCompactionGuard", () => { expect(setCompactionEnabled).toHaveBeenCalledWith(false); }); - it("disables Pi auto-compaction when a context engine plugin owns compaction", () => { + it("disables embedded auto-compaction when a context engine plugin owns compaction", () => { const setCompactionEnabled = vi.fn(); const settingsManager = { getCompactionReserveTokens: () => 20_000, @@ -533,7 +535,7 @@ describe("applyPiAutoCompactionGuard", () => { setCompactionEnabled, }; - const result = applyPiAutoCompactionGuard({ + const result = applyAgentAutoCompactionGuard({ settingsManager, contextEngineInfo: { id: "third-party", @@ -547,7 +549,7 @@ describe("applyPiAutoCompactionGuard", () => { expect(setCompactionEnabled).toHaveBeenCalledWith(false); }); - it("disables Pi auto-compaction when provider config forces safeguard mode", () => { + it("disables embedded auto-compaction when provider config forces safeguard mode", () => { const setCompactionEnabled = vi.fn(); const settingsManager = { getCompactionReserveTokens: () => 20_000, @@ -556,7 +558,7 @@ describe("applyPiAutoCompactionGuard", () => { setCompactionEnabled, }; - const result = applyPiAutoCompactionGuard({ + const result = applyAgentAutoCompactionGuard({ settingsManager, compactionMode: resolveEffectiveCompactionMode({ agents: { defaults: { compaction: { provider: "deepseek" } } }, @@ -567,11 +569,11 @@ describe("applyPiAutoCompactionGuard", () => { expect(setCompactionEnabled).toHaveBeenCalledWith(false); }); - // Default-mode runs against ordinary providers must keep Pi's auto-compaction - // enabled. Disabling it across the board would silently remove Pi's + // Default-mode runs against ordinary providers must keep OpenClaw runtime's auto-compaction + // enabled. Disabling it across the board would silently remove OpenClaw runtime's // overflow-recovery path inside Session.prompt() for users who are not // affected by z.ai's silent-overflow accounting. - it("leaves Pi auto-compaction alone for non-z.ai providers without engine ownership", () => { + it("leaves embedded auto-compaction alone for non-z.ai providers without engine ownership", () => { const setCompactionEnabled = vi.fn(); const settingsManager = { getCompactionReserveTokens: () => 20_000, @@ -580,7 +582,7 @@ describe("applyPiAutoCompactionGuard", () => { setCompactionEnabled, }; - const result = applyPiAutoCompactionGuard({ + const result = applyAgentAutoCompactionGuard({ settingsManager, contextEngineInfo: { id: "legacy", @@ -601,7 +603,7 @@ describe("applyPiAutoCompactionGuard", () => { applyOverrides: () => {}, }; - const result = applyPiAutoCompactionGuard({ + const result = applyAgentAutoCompactionGuard({ settingsManager, silentOverflowProneProvider: true, }); diff --git a/src/agents/pi-settings.ts b/src/agents/agent-settings.ts similarity index 86% rename from src/agents/pi-settings.ts rename to src/agents/agent-settings.ts index 8beb54d94b0..07a304e3403 100644 --- a/src/agents/pi-settings.ts +++ b/src/agents/agent-settings.ts @@ -1,13 +1,13 @@ import type { AgentCompactionMode } from "../config/types.agent-defaults.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { ContextEngineInfo } from "../context-engine/types.js"; -import { MIN_PROMPT_BUDGET_RATIO, MIN_PROMPT_BUDGET_TOKENS } from "./pi-compaction-constants.js"; +import { MIN_PROMPT_BUDGET_RATIO, MIN_PROMPT_BUDGET_TOKENS } from "./agent-compaction-constants.js"; import { resolveProviderEndpoint } from "./provider-attribution.js"; import { normalizeProviderId } from "./provider-id.js"; -export const DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR = 20_000; +export const DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR = 20_000; -type PiSettingsManagerLike = { +type AgentSettingsManagerLike = { getCompactionReserveTokens: () => number; getCompactionKeepRecentTokens: () => number; applyOverrides: (overrides: { @@ -25,11 +25,11 @@ type PiSettingsManagerLike = { * If called for small-context models without threading `contextTokenBudget`, * it may re-introduce context overflow issues. */ -export function ensurePiCompactionReserveTokens(params: { - settingsManager: PiSettingsManagerLike; +export function ensureAgentCompactionReserveTokens(params: { + settingsManager: AgentSettingsManagerLike; minReserveTokens?: number; }): { didOverride: boolean; reserveTokens: number } { - const minReserveTokens = params.minReserveTokens ?? DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR; + const minReserveTokens = params.minReserveTokens ?? DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR; const current = params.settingsManager.getCompactionReserveTokens(); if (current >= minReserveTokens) { @@ -48,7 +48,7 @@ export function resolveCompactionReserveTokensFloor(cfg?: OpenClawConfig): numbe if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) { return Math.floor(raw); } - return DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR; + return DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR; } function toNonNegativeInt(value: unknown): number | undefined { @@ -65,8 +65,8 @@ function toPositiveInt(value: unknown): number | undefined { return Math.floor(value); } -export function applyPiCompactionSettingsFromConfig(params: { - settingsManager: PiSettingsManagerLike; +export function applyAgentCompactionSettingsFromConfig(params: { + settingsManager: AgentSettingsManagerLike; cfg?: OpenClawConfig; /** When known, the resolved context window budget for the current model. */ contextTokenBudget?: number; @@ -135,8 +135,8 @@ export function resolveEffectiveCompactionMode(cfg?: OpenClawConfig): AgentCompa } /** - * Detect providers whose pi-ai `isContextOverflow` Case 2 (silent overflow) - * fires on a successful turn and triggers Pi's `_runAutoCompaction` from + * Detect providers whose shared model runtime `isContextOverflow` Case 2 (silent overflow) + * fires on a successful turn and triggers OpenClaw runtime's `_runAutoCompaction` from * inside `Session.prompt()`, collapsing `agent.state.messages` before the * provider call (openclaw#75799). * @@ -178,15 +178,15 @@ export function isSilentOverflowProneModel(model: { } /** - * Disable Pi's `_checkCompaction → _runAutoCompaction` (which would otherwise + * Disable OpenClaw runtime's `_checkCompaction → _runAutoCompaction` (which would otherwise * fire from inside `Session.prompt()` and reassign `agent.state.messages` * before the provider call) when OpenClaw or a plugin owns compaction: * `contextEngineInfo.ownsCompaction === true`, effective safeguard compaction, * or an active model that is silent-overflow-prone (openclaw#75799). - * Default-mode runs against ordinary providers keep Pi's auto-compaction as + * Default-mode runs against ordinary providers keep OpenClaw runtime's auto-compaction as * the existing baseline. */ -export function shouldDisablePiAutoCompaction(params: { +export function shouldDisableAgentAutoCompaction(params: { contextEngineInfo?: ContextEngineInfo; compactionMode?: AgentCompactionMode; silentOverflowProneProvider?: boolean; @@ -201,17 +201,17 @@ export function shouldDisablePiAutoCompaction(params: { /** * Apply the auto-compaction guard. Callers that reload a `DefaultResourceLoader` * MUST call this AGAIN after each `reload()` — `settingsManager.reload()` - * rehydrates `compaction.enabled` from disk and silently restores Pi's + * rehydrates `compaction.enabled` from disk and silently restores OpenClaw runtime's * default-on behavior, undoing the guard. Mirrors the existing - * `applyPiCompactionSettingsFromConfig` re-call pattern at the same sites. + * `applyAgentCompactionSettingsFromConfig` re-call pattern at the same sites. */ -export function applyPiAutoCompactionGuard(params: { - settingsManager: PiSettingsManagerLike; +export function applyAgentAutoCompactionGuard(params: { + settingsManager: AgentSettingsManagerLike; contextEngineInfo?: ContextEngineInfo; compactionMode?: AgentCompactionMode; silentOverflowProneProvider?: boolean; }): { supported: boolean; disabled: boolean } { - const disable = shouldDisablePiAutoCompaction({ + const disable = shouldDisableAgentAutoCompaction({ contextEngineInfo: params.contextEngineInfo, compactionMode: params.compactionMode, silentOverflowProneProvider: params.silentOverflowProneProvider, diff --git a/src/agents/pi-tool-definition-adapter.after-tool-call.fires-once.test.ts b/src/agents/agent-tool-definition-adapter.after-tool-call.fires-once.test.ts similarity index 93% rename from src/agents/pi-tool-definition-adapter.after-tool-call.fires-once.test.ts rename to src/agents/agent-tool-definition-adapter.after-tool-call.fires-once.test.ts index 801ceea879a..36140d2bcc8 100644 --- a/src/agents/pi-tool-definition-adapter.after-tool-call.fires-once.test.ts +++ b/src/agents/agent-tool-definition-adapter.after-tool-call.fires-once.test.ts @@ -6,10 +6,10 @@ * Regression guard for the double-fire bug fixed by removing the adapter-side * after_tool_call invocation (see PR #27283 → dedup in this fix). */ -import type { AgentTool } from "@earendil-works/pi-agent-core"; +import type { AgentTool } from "openclaw/plugin-sdk/agent-core"; import { Type } from "typebox"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { createBaseToolHandlerState } from "./pi-tool-handler-state.test-helpers.js"; +import { createBaseToolHandlerState } from "./agent-tool-handler-state.test-helpers.js"; const hookMocks = vi.hoisted(() => ({ runner: { @@ -84,9 +84,9 @@ function createToolHandlerCtx() { }; } -let toToolDefinitions: typeof import("./pi-tool-definition-adapter.js").toToolDefinitions; -let handleToolExecutionStart: typeof import("./pi-embedded-subscribe.handlers.tools.js").handleToolExecutionStart; -let handleToolExecutionEnd: typeof import("./pi-embedded-subscribe.handlers.tools.js").handleToolExecutionEnd; +let toToolDefinitions: typeof import("./agent-tool-definition-adapter.js").toToolDefinitions; +let handleToolExecutionStart: typeof import("./embedded-agent-subscribe.handlers.tools.js").handleToolExecutionStart; +let handleToolExecutionEnd: typeof import("./embedded-agent-subscribe.handlers.tools.js").handleToolExecutionEnd; async function loadFreshAfterToolCallModulesForTest() { vi.doMock("../plugins/hook-runner-global.js", () => ({ @@ -97,7 +97,7 @@ async function loadFreshAfterToolCallModulesForTest() { emitAgentEvent: vi.fn(), emitAgentItemEvent: vi.fn(), })); - vi.doMock("./pi-tools.before-tool-call.js", () => ({ + vi.doMock("./agent-tools.before-tool-call.js", () => ({ BeforeToolCallBlockedError: beforeToolCallMocks.BeforeToolCallBlockedError, buildBlockedToolResult: ({ reason }: { reason: string }) => ({ content: [{ type: "text", text: reason }], @@ -110,9 +110,9 @@ async function loadFreshAfterToolCallModulesForTest() { isToolWrappedWithBeforeToolCallHook: beforeToolCallMocks.isToolWrappedWithBeforeToolCallHook, runBeforeToolCallHook: beforeToolCallMocks.runBeforeToolCallHook, })); - ({ toToolDefinitions } = await import("./pi-tool-definition-adapter.js")); + ({ toToolDefinitions } = await import("./agent-tool-definition-adapter.js")); ({ handleToolExecutionStart, handleToolExecutionEnd } = - await import("./pi-embedded-subscribe.handlers.tools.js")); + await import("./embedded-agent-subscribe.handlers.tools.js")); } describe("after_tool_call fires exactly once in embedded runs", () => { diff --git a/src/agents/pi-tool-definition-adapter.after-tool-call.test.ts b/src/agents/agent-tool-definition-adapter.after-tool-call.test.ts similarity index 94% rename from src/agents/pi-tool-definition-adapter.after-tool-call.test.ts rename to src/agents/agent-tool-definition-adapter.after-tool-call.test.ts index 4f4a0a59aca..2d5c9db1615 100644 --- a/src/agents/pi-tool-definition-adapter.after-tool-call.test.ts +++ b/src/agents/agent-tool-definition-adapter.after-tool-call.test.ts @@ -1,7 +1,7 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; +import type { AgentTool } from "openclaw/plugin-sdk/agent-core"; import { Type } from "typebox"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { toToolDefinitions } from "./pi-tool-definition-adapter.js"; +import { toToolDefinitions } from "./agent-tool-definition-adapter.js"; const hookMocks = vi.hoisted(() => ({ runner: { @@ -30,7 +30,7 @@ vi.mock("../plugins/hook-runner-global.js", () => ({ getGlobalHookRunner: () => hookMocks.runner, })); -vi.mock("./pi-tools.before-tool-call.js", () => ({ +vi.mock("./agent-tools.before-tool-call.js", () => ({ BeforeToolCallBlockedError: hookMocks.BeforeToolCallBlockedError, buildBlockedToolResult: ({ reason }: { reason: string }) => ({ content: [{ type: "text", text: reason }], @@ -57,7 +57,7 @@ function createReadTool() { type ToolExecute = ReturnType[number]["execute"]; const extensionContext = {} as Parameters[4]; -describe("pi tool definition adapter after_tool_call", () => { +describe("agent tool definition adapter after_tool_call", () => { beforeEach(() => { hookMocks.runner.hasHooks.mockClear(); hookMocks.runner.runAfterToolCall.mockClear(); diff --git a/src/agents/pi-tool-definition-adapter.logging.test.ts b/src/agents/agent-tool-definition-adapter.logging.test.ts similarity index 93% rename from src/agents/pi-tool-definition-adapter.logging.test.ts rename to src/agents/agent-tool-definition-adapter.logging.test.ts index 9c2c935b1dc..646865942ef 100644 --- a/src/agents/pi-tool-definition-adapter.logging.test.ts +++ b/src/agents/agent-tool-definition-adapter.logging.test.ts @@ -1,4 +1,4 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; +import type { AgentTool } from "openclaw/plugin-sdk/agent-core"; import { Type } from "typebox"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -12,14 +12,14 @@ vi.mock("../logger.js", () => ({ logError: mocks.logError, })); -let toToolDefinitions: typeof import("./pi-tool-definition-adapter.js").toToolDefinitions; -let BeforeToolCallBlockedError: typeof import("./pi-tools.before-tool-call.js").BeforeToolCallBlockedError; -let wrapToolParamValidation: typeof import("./pi-tools.params.js").wrapToolParamValidation; -let REQUIRED_PARAM_GROUPS: typeof import("./pi-tools.params.js").REQUIRED_PARAM_GROUPS; +let toToolDefinitions: typeof import("./agent-tool-definition-adapter.js").toToolDefinitions; +let BeforeToolCallBlockedError: typeof import("./agent-tools.before-tool-call.js").BeforeToolCallBlockedError; +let wrapToolParamValidation: typeof import("./agent-tools.params.js").wrapToolParamValidation; +let REQUIRED_PARAM_GROUPS: typeof import("./agent-tools.params.js").REQUIRED_PARAM_GROUPS; let logError: typeof import("../logger.js").logError; type ToolExecute = ReturnType< - typeof import("./pi-tool-definition-adapter.js").toToolDefinitions + typeof import("./agent-tool-definition-adapter.js").toToolDefinitions >[number]["execute"]; const extensionContext = {} as Parameters[4]; @@ -27,11 +27,11 @@ function firstLogErrorMessage(): unknown { return vi.mocked(logError).mock.calls[0]?.[0]; } -describe("pi tool definition adapter logging", () => { +describe("agent tool definition adapter logging", () => { beforeAll(async () => { - ({ toToolDefinitions } = await import("./pi-tool-definition-adapter.js")); - ({ BeforeToolCallBlockedError } = await import("./pi-tools.before-tool-call.js")); - ({ wrapToolParamValidation, REQUIRED_PARAM_GROUPS } = await import("./pi-tools.params.js")); + ({ toToolDefinitions } = await import("./agent-tool-definition-adapter.js")); + ({ BeforeToolCallBlockedError } = await import("./agent-tools.before-tool-call.js")); + ({ wrapToolParamValidation, REQUIRED_PARAM_GROUPS } = await import("./agent-tools.params.js")); ({ logError } = await import("../logger.js")); }); diff --git a/src/agents/pi-tool-definition-adapter.test.ts b/src/agents/agent-tool-definition-adapter.test.ts similarity index 96% rename from src/agents/pi-tool-definition-adapter.test.ts rename to src/agents/agent-tool-definition-adapter.test.ts index 036dc86aa36..21b3829e544 100644 --- a/src/agents/pi-tool-definition-adapter.test.ts +++ b/src/agents/agent-tool-definition-adapter.test.ts @@ -1,7 +1,6 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; +import type { AgentTool } from "openclaw/plugin-sdk/agent-core"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; -import type { ClientToolDefinition } from "./pi-embedded-runner/run/params.js"; import { CLIENT_TOOL_NAME_CONFLICT_PREFIX, createClientToolNameConflictError, @@ -9,7 +8,8 @@ import { isClientToolNameConflictError, toClientToolDefinitions, toToolDefinitions, -} from "./pi-tool-definition-adapter.js"; +} from "./agent-tool-definition-adapter.js"; +import type { ClientToolDefinition } from "./embedded-agent-runner/run/params.js"; type ToolExecute = ReturnType[number]["execute"]; const extensionContext = {} as Parameters[4]; @@ -42,7 +42,7 @@ async function executeTool(tool: AgentTool, callId: string) { return await def.execute(callId, {}, undefined, undefined, extensionContext); } -describe("pi tool definition adapter", () => { +describe("agent tool definition adapter", () => { it("wraps tool errors into a tool result", async () => { const result = await executeThrowingTool("boom", "call1"); @@ -232,7 +232,7 @@ describe("client tool name conflict checks", () => { ).toEqual(["Weather", "weather"]); }); - it("detects collisions with reserved Pi built-in tool names", () => { + it("detects collisions with reserved OpenClaw built-in tool names", () => { expect( findClientToolNameConflicts({ tools: [makeClientTool("Bash"), makeClientTool("grep")], diff --git a/src/agents/pi-tool-definition-adapter.ts b/src/agents/agent-tool-definition-adapter.ts similarity index 96% rename from src/agents/pi-tool-definition-adapter.ts rename to src/agents/agent-tool-definition-adapter.ts index ecf7a1f6bc7..6bf94d6b7bd 100644 --- a/src/agents/pi-tool-definition-adapter.ts +++ b/src/agents/agent-tool-definition-adapter.ts @@ -1,28 +1,24 @@ import { createHash } from "node:crypto"; -import type { - AgentTool, - AgentToolResult, - AgentToolUpdateCallback, -} from "@earendil-works/pi-agent-core"; -import type { ToolDefinition } from "@earendil-works/pi-coding-agent"; import { logDebug, logError } from "../logger.js"; import { redactToolDetail } from "../logging/redact.js"; import { isPlainObject } from "../utils.js"; -import { - getCodeModeExecBeforeHookMetadata, - normalizeCodeModeExecBeforeHookParams, - reconcileCodeModeExecBeforeHookParams, -} from "./code-mode-control-tools.js"; -import { sanitizeForConsole } from "./console-sanitize.js"; -import type { ClientToolDefinition } from "./pi-embedded-runner/run/params.js"; -import type { HookContext } from "./pi-tools.before-tool-call.js"; +import type { HookContext } from "./agent-tools.before-tool-call.js"; import { buildBlockedToolResult, isToolWrappedWithBeforeToolCallHook, isBeforeToolCallBlockedError, recordAdjustedParamsForToolCall, runBeforeToolCallHook, -} from "./pi-tools.before-tool-call.js"; +} from "./agent-tools.before-tool-call.js"; +import { + getCodeModeExecBeforeHookMetadata, + normalizeCodeModeExecBeforeHookParams, + reconcileCodeModeExecBeforeHookParams, +} from "./code-mode-control-tools.js"; +import { sanitizeForConsole } from "./console-sanitize.js"; +import type { ClientToolDefinition } from "./embedded-agent-runner/run/params.js"; +import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "./runtime/index.js"; +import type { ToolDefinition } from "./sessions/index.js"; import { normalizeToolName } from "./tool-policy.js"; import { jsonResult, payloadTextResult } from "./tools/common.js"; @@ -32,13 +28,13 @@ type ToolExecuteArgsCurrent = [ string, unknown, AbortSignal | undefined, - AgentToolUpdateCallback | undefined, + AgentToolUpdateCallback | undefined, unknown, ]; type ToolExecuteArgsLegacy = [ string, unknown, - AgentToolUpdateCallback | undefined, + AgentToolUpdateCallback | undefined, unknown, AbortSignal | undefined, ]; @@ -252,7 +248,7 @@ function buildToolExecutionErrorResult(params: { function splitToolExecuteArgs(args: ToolExecuteArgsAny): { toolCallId: string; params: unknown; - onUpdate: AgentToolUpdateCallback | undefined; + onUpdate: AgentToolUpdateCallback | undefined; signal: AbortSignal | undefined; } { if (isLegacyToolExecuteArgs(args)) { diff --git a/src/agents/pi-tool-handler-state.test-helpers.ts b/src/agents/agent-tool-handler-state.test-helpers.ts similarity index 92% rename from src/agents/pi-tool-handler-state.test-helpers.ts rename to src/agents/agent-tool-handler-state.test-helpers.ts index dca29aad3a5..54ffca0161f 100644 --- a/src/agents/pi-tool-handler-state.test-helpers.ts +++ b/src/agents/agent-tool-handler-state.test-helpers.ts @@ -1,4 +1,4 @@ -import { createEmbeddedRunReplayState } from "./pi-embedded-runner/replay-state.js"; +import { createEmbeddedRunReplayState } from "./embedded-agent-runner/replay-state.js"; export function createBaseToolHandlerState() { return { diff --git a/src/agents/pi-tools-agent-config.exec.test.ts b/src/agents/agent-tools-agent-config.exec.test.ts similarity index 98% rename from src/agents/pi-tools-agent-config.exec.test.ts rename to src/agents/agent-tools-agent-config.exec.test.ts index 268e72e246d..f5c50725401 100644 --- a/src/agents/pi-tools-agent-config.exec.test.ts +++ b/src/agents/agent-tools-agent-config.exec.test.ts @@ -4,7 +4,7 @@ import "./test-helpers/fast-openclaw-tools.js"; import type { OpenClawConfig } from "../config/config.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createSessionConversationTestRegistry } from "../test-utils/session-conversation-registry.js"; -import { createOpenClawCodingTools } from "./pi-tools.js"; +import { createOpenClawCodingTools } from "./agent-tools.js"; function createExecHostDefaultsConfig( agents: Array<{ id: string; execHost?: "auto" | "gateway" | "sandbox" }>, diff --git a/src/agents/pi-tools-agent-config.test.ts b/src/agents/agent-tools-agent-config.test.ts similarity index 99% rename from src/agents/pi-tools-agent-config.test.ts rename to src/agents/agent-tools-agent-config.test.ts index 5f370fb5956..5a0a51ffccf 100644 --- a/src/agents/pi-tools-agent-config.test.ts +++ b/src/agents/agent-tools-agent-config.test.ts @@ -9,8 +9,8 @@ import type { OpenClawConfig } from "../config/config.js"; import { resolveChannelGroupToolsPolicy } from "../config/group-policy.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createSessionConversationTestRegistry } from "../test-utils/session-conversation-registry.js"; -import { createOpenClawCodingTools } from "./pi-tools.js"; -import { resolveEffectiveToolPolicy } from "./pi-tools.policy.js"; +import { createOpenClawCodingTools } from "./agent-tools.js"; +import { resolveEffectiveToolPolicy } from "./agent-tools.policy.js"; import type { SandboxDockerConfig } from "./sandbox.js"; import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; import { createRestrictedAgentSandboxConfig } from "./test-helpers/sandbox-agent-config-fixtures.js"; @@ -56,7 +56,7 @@ describe("Agent-specific tool filtering", () => { patch: string; }) => Promise, ) { - const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pi-tools-")); + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-tools-")); const escapedPath = path.join( path.dirname(workspaceDir), `escaped-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`, diff --git a/src/agents/pi-tools-parameter-schema.ts b/src/agents/agent-tools-parameter-schema.ts similarity index 100% rename from src/agents/pi-tools-parameter-schema.ts rename to src/agents/agent-tools-parameter-schema.ts diff --git a/src/agents/pi-tools.abort.ts b/src/agents/agent-tools.abort.ts similarity index 93% rename from src/agents/pi-tools.abort.ts rename to src/agents/agent-tools.abort.ts index 13d949891cf..7f29222455e 100644 --- a/src/agents/pi-tools.abort.ts +++ b/src/agents/agent-tools.abort.ts @@ -1,8 +1,8 @@ import { copyPluginToolMeta } from "../plugins/tools.js"; import { bindAbortRelay } from "../utils/fetch-timeout.js"; +import { copyBeforeToolCallHookMarker } from "./agent-tools.before-tool-call.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; import { copyChannelAgentToolMeta } from "./channel-tools.js"; -import { copyBeforeToolCallHookMarker } from "./pi-tools.before-tool-call.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; function throwAbortError(): never { const err = new Error("Aborted"); diff --git a/src/agents/pi-tools.availability.test.ts b/src/agents/agent-tools.availability.test.ts similarity index 96% rename from src/agents/pi-tools.availability.test.ts rename to src/agents/agent-tools.availability.test.ts index 80492b549cf..831247f742b 100644 --- a/src/agents/pi-tools.availability.test.ts +++ b/src/agents/agent-tools.availability.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import "./test-helpers/fast-coding-tools.js"; import "./test-helpers/fast-openclaw-tools.js"; -import { createOpenClawCodingTools } from "./pi-tools.js"; +import { createOpenClawCodingTools } from "./agent-tools.js"; vi.mock("./channel-tools.js", () => { const passthrough = (tool: T) => tool; diff --git a/src/agents/pi-tools.before-tool-call.e2e.test.ts b/src/agents/agent-tools.before-tool-call.e2e.test.ts similarity index 99% rename from src/agents/pi-tools.before-tool-call.e2e.test.ts rename to src/agents/agent-tools.before-tool-call.e2e.test.ts index da43ead539f..8affa1d93da 100644 --- a/src/agents/pi-tools.before-tool-call.e2e.test.ts +++ b/src/agents/agent-tools.before-tool-call.e2e.test.ts @@ -16,7 +16,7 @@ import { setPluginToolMeta } from "../plugins/tools.js"; import { runBeforeToolCallHook, wrapToolWithBeforeToolCallHook, -} from "./pi-tools.before-tool-call.js"; +} from "./agent-tools.before-tool-call.js"; import { createCanonicalFixtureSkill } from "./skills.test-helpers.js"; import { CRITICAL_THRESHOLD } from "./tool-loop-detection.js"; import type { AnyAgentTool } from "./tools/common.js"; diff --git a/src/agents/pi-tools.before-tool-call.embedded-mode.test.ts b/src/agents/agent-tools.before-tool-call.embedded-mode.test.ts similarity index 99% rename from src/agents/pi-tools.before-tool-call.embedded-mode.test.ts rename to src/agents/agent-tools.before-tool-call.embedded-mode.test.ts index 5915ea9a6eb..a46524efa61 100644 --- a/src/agents/pi-tools.before-tool-call.embedded-mode.test.ts +++ b/src/agents/agent-tools.before-tool-call.embedded-mode.test.ts @@ -5,7 +5,7 @@ import type { HookRunner } from "../plugins/hooks.js"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { PluginApprovalResolutions } from "../plugins/types.js"; -import { runBeforeToolCallHook } from "./pi-tools.before-tool-call.js"; +import { runBeforeToolCallHook } from "./agent-tools.before-tool-call.js"; import { callGatewayTool } from "./tools/gateway.js"; vi.mock("../plugins/hook-runner-global.js", async () => { diff --git a/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts b/src/agents/agent-tools.before-tool-call.integration.e2e.test.ts similarity index 99% rename from src/agents/pi-tools.before-tool-call.integration.e2e.test.ts rename to src/agents/agent-tools.before-tool-call.integration.e2e.test.ts index 3d9056a3005..f5342f19881 100644 --- a/src/agents/pi-tools.before-tool-call.integration.e2e.test.ts +++ b/src/agents/agent-tools.before-tool-call.integration.e2e.test.ts @@ -13,17 +13,17 @@ import { patchPluginSessionExtension } from "../plugins/host-hook-state.js"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import type { PluginHookRegistration } from "../plugins/types.js"; -import { markCodeModeControlTool } from "./code-mode-control-tools.js"; -import { CODE_MODE_EXEC_TOOL_NAME, createCodeModeTools } from "./code-mode.js"; -import { splitSdkTools } from "./pi-embedded-runner.js"; -import { toClientToolDefinitions, toToolDefinitions } from "./pi-tool-definition-adapter.js"; -import { wrapToolWithAbortSignal } from "./pi-tools.abort.js"; +import { toClientToolDefinitions, toToolDefinitions } from "./agent-tool-definition-adapter.js"; +import { wrapToolWithAbortSignal } from "./agent-tools.abort.js"; import { testing as beforeToolCallTesting, consumeAdjustedParamsForToolCall, isToolWrappedWithBeforeToolCallHook, wrapToolWithBeforeToolCallHook, -} from "./pi-tools.before-tool-call.js"; +} from "./agent-tools.before-tool-call.js"; +import { markCodeModeControlTool } from "./code-mode-control-tools.js"; +import { CODE_MODE_EXEC_TOOL_NAME, createCodeModeTools } from "./code-mode.js"; +import { splitSdkTools } from "./embedded-agent-runner.js"; type BeforeToolCallHandlerMock = ReturnType; @@ -292,7 +292,7 @@ describe("before_tool_call hook deduplication (#15502)", () => { expect(beforeToolCallHook).toHaveBeenCalledTimes(1); }); - it("passes agent context to outer code-mode exec hooks through Pi custom tools", async () => { + it("passes agent context to outer code-mode exec hooks through OpenClaw custom tools", async () => { beforeToolCallHook = installBeforeToolCallHook({ runBeforeToolCallImpl: async () => ({ block: true, diff --git a/src/agents/pi-tools.before-tool-call.runtime.ts b/src/agents/agent-tools.before-tool-call.runtime.ts similarity index 100% rename from src/agents/pi-tools.before-tool-call.runtime.ts rename to src/agents/agent-tools.before-tool-call.runtime.ts diff --git a/src/agents/pi-tools.before-tool-call.state.ts b/src/agents/agent-tools.before-tool-call.state.ts similarity index 100% rename from src/agents/pi-tools.before-tool-call.state.ts rename to src/agents/agent-tools.before-tool-call.state.ts diff --git a/src/agents/pi-tools.before-tool-call.ts b/src/agents/agent-tools.before-tool-call.ts similarity index 99% rename from src/agents/pi-tools.before-tool-call.ts rename to src/agents/agent-tools.before-tool-call.ts index 07a78e54675..90c2aecd465 100644 --- a/src/agents/pi-tools.before-tool-call.ts +++ b/src/agents/agent-tools.before-tool-call.ts @@ -32,6 +32,7 @@ import { } from "../plugins/types.js"; import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js"; import { isPlainObject } from "../utils.js"; +import { adjustedParamsByToolCallId } from "./agent-tools.before-tool-call.state.js"; import { copyChannelAgentToolMeta, getChannelAgentToolMeta } from "./channel-tools.js"; import { getCodeModeExecBeforeHookMetadata, @@ -40,7 +41,6 @@ import { normalizeCodeModeExecBeforeHookParamsForToolKind, reconcileCodeModeExecBeforeHookParams, } from "./code-mode-control-tools.js"; -import { adjustedParamsByToolCallId } from "./pi-tools.before-tool-call.state.js"; import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; import { resolveSkillTelemetrySource, resolveSkillTelemetrySourceValue } from "./skills/source.js"; import type { SkillSnapshot, SkillTelemetrySource } from "./skills/types.js"; @@ -191,7 +191,7 @@ export function isBeforeToolCallBlockedError(err: unknown): err is BeforeToolCal } const loadBeforeToolCallRuntime = createLazyRuntimeSurface( - () => import("./pi-tools.before-tool-call.runtime.js"), + () => import("./agent-tools.before-tool-call.runtime.js"), ({ beforeToolCallRuntime }) => beforeToolCallRuntime, ); diff --git a/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts b/src/agents/agent-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts similarity index 98% rename from src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts rename to src/agents/agent-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts index d939600a30d..bcbe3f70ccd 100644 --- a/src/agents/pi-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts +++ b/src/agents/agent-tools.create-openclaw-coding-tools.adds-claude-style-aliases-schemas-without-dropping-g.test.ts @@ -1,10 +1,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core"; +import type { AgentTool, AgentToolResult } from "openclaw/plugin-sdk/agent-core"; import { Type } from "typebox"; import { describe, expect, it, vi } from "vitest"; -import { createOpenClawReadTool, createSandboxedReadTool } from "./pi-tools.read.js"; +import { createOpenClawReadTool, createSandboxedReadTool } from "./agent-tools.read.js"; import { createHostSandboxFsBridge } from "./test-helpers/host-sandbox-fs-bridge.js"; function extractToolText(result: unknown): string { diff --git a/src/agents/pi-tools.create-openclaw-coding-tools.test.ts b/src/agents/agent-tools.create-openclaw-coding-tools.test.ts similarity index 98% rename from src/agents/pi-tools.create-openclaw-coding-tools.test.ts rename to src/agents/agent-tools.create-openclaw-coding-tools.test.ts index 1c654958838..9a3382bfe05 100644 --- a/src/agents/pi-tools.create-openclaw-coding-tools.test.ts +++ b/src/agents/agent-tools.create-openclaw-coding-tools.test.ts @@ -12,16 +12,16 @@ import { resetGlobalHookRunner, } from "../plugins/hook-runner-global.js"; import { createMockPluginRegistry } from "../plugins/hooks.test-helpers.js"; -import type { AuthProfileStore } from "./auth-profiles/types.js"; +import { createOpenClawCodingTools } from "./agent-tools.js"; import "./test-helpers/fast-bash-tools.js"; import "./test-helpers/fast-coding-tools.js"; import "./test-helpers/fast-openclaw-tools.js"; +import type { AuthProfileStore } from "./auth-profiles/types.js"; import * as openClawPluginTools from "./openclaw-plugin-tools.js"; import { createOpenClawTools } from "./openclaw-tools.js"; -import { createOpenClawCodingTools } from "./pi-tools.js"; +import { expectReadWriteEditTools } from "./test-helpers/agent-tools-fs-helpers.js"; +import { createAgentToolsSandboxContext } from "./test-helpers/agent-tools-sandbox-context.js"; import { createHostSandboxFsBridge } from "./test-helpers/host-sandbox-fs-bridge.js"; -import { expectReadWriteEditTools } from "./test-helpers/pi-tools-fs-helpers.js"; -import { createPiToolsSandboxContext } from "./test-helpers/pi-tools-sandbox-context.js"; import { providerAliasCases } from "./test-helpers/provider-alias-cases.js"; import { buildEmptyExplicitToolAllowlistError } from "./tool-allowlist-guard.js"; import { DEFAULT_PLUGIN_TOOLS_ALLOWLIST_ENTRY, normalizeToolName } from "./tool-policy.js"; @@ -209,7 +209,7 @@ describe("createOpenClawCodingTools", () => { ); }); - it("adds PI Tool Search control tools when explicitly requested", () => { + it("adds Tool Search control tools when explicitly requested", () => { const tools = createOpenClawCodingTools({ includeToolSearchControls: true, config: { @@ -226,7 +226,7 @@ describe("createOpenClawCodingTools", () => { expect(names.has("tool_call")).toBe(true); }); - it("keeps PI Tool Search controls available under restrictive tool profiles", () => { + it("keeps Tool Search controls available under restrictive tool profiles", () => { const tools = createOpenClawCodingTools({ includeToolSearchControls: true, config: { @@ -245,7 +245,7 @@ describe("createOpenClawCodingTools", () => { expect(names.has("message")).toBe(false); }); - it("keeps PI Tool Search controls available under restrictive tool allowlists", () => { + it("keeps Tool Search controls available under restrictive tool allowlists", () => { const tools = createOpenClawCodingTools({ includeToolSearchControls: true, config: { @@ -265,7 +265,7 @@ describe("createOpenClawCodingTools", () => { expect(names.has("tool_call")).toBe(true); }); - it("lets explicit deny policies remove PI Tool Search controls", () => { + it("lets explicit deny policies remove Tool Search controls", () => { const tools = createOpenClawCodingTools({ includeToolSearchControls: true, config: { @@ -282,7 +282,7 @@ describe("createOpenClawCodingTools", () => { expect(names.has("read")).toBe(true); }); - it("keeps PI Tool Search controls when core OpenClaw tools are not materialized", () => { + it("keeps Tool Search controls when core OpenClaw tools are not materialized", () => { const createOpenClawToolsMock = vi.mocked(createOpenClawTools); createOpenClawToolsMock.mockClear(); @@ -1212,7 +1212,7 @@ describe("createOpenClawCodingTools", () => { it("filters tools by sandbox policy", () => { const sandboxDir = path.join(os.tmpdir(), "openclaw-sandbox"); - const sandbox = createPiToolsSandboxContext({ + const sandbox = createAgentToolsSandboxContext({ workspaceDir: sandboxDir, agentWorkspaceDir: path.join(os.tmpdir(), "openclaw-workspace"), workspaceAccess: "none" as const, @@ -1230,7 +1230,7 @@ describe("createOpenClawCodingTools", () => { it("hard-disables write/edit when sandbox workspaceAccess is ro", () => { const sandboxDir = path.join(os.tmpdir(), "openclaw-sandbox"); - const sandbox = createPiToolsSandboxContext({ + const sandbox = createAgentToolsSandboxContext({ workspaceDir: sandboxDir, agentWorkspaceDir: path.join(os.tmpdir(), "openclaw-workspace"), workspaceAccess: "ro" as const, diff --git a/src/agents/pi-tools.cron-scope.test.ts b/src/agents/agent-tools.cron-scope.test.ts similarity index 96% rename from src/agents/pi-tools.cron-scope.test.ts rename to src/agents/agent-tools.cron-scope.test.ts index 9e09d48532e..2ece96e0362 100644 --- a/src/agents/pi-tools.cron-scope.test.ts +++ b/src/agents/agent-tools.cron-scope.test.ts @@ -27,7 +27,7 @@ vi.mock("./openclaw-tools.js", () => ({ import "./test-helpers/fast-bash-tools.js"; import "./test-helpers/fast-coding-tools.js"; -import { createOpenClawCodingTools } from "./pi-tools.js"; +import { createOpenClawCodingTools } from "./agent-tools.js"; function firstOpenClawToolsOptions(): { cronSelfRemoveOnlyJobId?: string } | undefined { return mocks.createOpenClawToolsOptions.mock.calls[0]?.[0] as diff --git a/src/agents/pi-tools.deferred-followup-guidance.test.ts b/src/agents/agent-tools.deferred-followup-guidance.test.ts similarity index 95% rename from src/agents/pi-tools.deferred-followup-guidance.test.ts rename to src/agents/agent-tools.deferred-followup-guidance.test.ts index 940869c94f8..60ccdb5e9fd 100644 --- a/src/agents/pi-tools.deferred-followup-guidance.test.ts +++ b/src/agents/agent-tools.deferred-followup-guidance.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { applyDeferredFollowupToolDescriptions } from "./pi-tools.deferred-followup.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +import { applyDeferredFollowupToolDescriptions } from "./agent-tools.deferred-followup.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; function findToolDescription(toolName: string, includeCron: boolean) { const tools = applyDeferredFollowupToolDescriptions([ diff --git a/src/agents/pi-tools.deferred-followup.ts b/src/agents/agent-tools.deferred-followup.ts similarity index 91% rename from src/agents/pi-tools.deferred-followup.ts rename to src/agents/agent-tools.deferred-followup.ts index ff582a3c2cb..6cc0980b678 100644 --- a/src/agents/pi-tools.deferred-followup.ts +++ b/src/agents/agent-tools.deferred-followup.ts @@ -1,5 +1,5 @@ +import type { AnyAgentTool } from "./agent-tools.types.js"; import { describeExecTool, describeProcessTool } from "./bash-tools.descriptions.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; export function applyDeferredFollowupToolDescriptions( tools: AnyAgentTool[], diff --git a/src/agents/pi-tools.host-edit.ts b/src/agents/agent-tools.host-edit.ts similarity index 97% rename from src/agents/pi-tools.host-edit.ts rename to src/agents/agent-tools.host-edit.ts index d66a338b5c6..4237b4a8368 100644 --- a/src/agents/pi-tools.host-edit.ts +++ b/src/agents/agent-tools.host-edit.ts @@ -1,9 +1,9 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core"; import { expandHomePrefix, resolveOsHomeDir } from "../infra/home-dir.js"; -import { getToolParamsRecord } from "./pi-tools.params.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +import { getToolParamsRecord } from "./agent-tools.params.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; +import type { AgentToolResult, AgentToolUpdateCallback } from "./runtime/index.js"; type EditToolRecoveryOptions = { root: string; @@ -303,7 +303,7 @@ export function wrapEditToolWithRecovery( toolCallId: string, params: unknown, signal: AbortSignal | undefined, - onUpdate?: AgentToolUpdateCallback, + onUpdate?: AgentToolUpdateCallback, ) => { const { pathParam, edits } = readEditToolParams(params); const absolutePath = diff --git a/src/agents/pi-tools.message-provider-policy.test.ts b/src/agents/agent-tools.message-provider-policy.test.ts similarity index 88% rename from src/agents/pi-tools.message-provider-policy.test.ts rename to src/agents/agent-tools.message-provider-policy.test.ts index f033eddf9d2..3ccb21dfa59 100644 --- a/src/agents/pi-tools.message-provider-policy.test.ts +++ b/src/agents/agent-tools.message-provider-policy.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { filterToolNamesByMessageProvider } from "./pi-tools.message-provider-policy.js"; +import { filterToolNamesByMessageProvider } from "./agent-tools.message-provider-policy.js"; const DEFAULT_TOOL_NAMES = ["read", "write", "tts", "web_search"]; diff --git a/src/agents/pi-tools.message-provider-policy.ts b/src/agents/agent-tools.message-provider-policy.ts similarity index 100% rename from src/agents/pi-tools.message-provider-policy.ts rename to src/agents/agent-tools.message-provider-policy.ts diff --git a/src/agents/pi-tools.model-provider-collision.test.ts b/src/agents/agent-tools.model-provider-collision.test.ts similarity index 98% rename from src/agents/pi-tools.model-provider-collision.test.ts rename to src/agents/agent-tools.model-provider-collision.test.ts index 48eb7c46295..de59212fa0a 100644 --- a/src/agents/pi-tools.model-provider-collision.test.ts +++ b/src/agents/agent-tools.model-provider-collision.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { testing } from "./pi-tools.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +import { testing } from "./agent-tools.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; const HTML_ENTITY_TOOL_CALL_ARGUMENTS_ENCODING = "html-entities"; const XAI_TOOL_SCHEMA_PROFILE = "xai"; diff --git a/src/agents/pi-tools.params.test.ts b/src/agents/agent-tools.params.test.ts similarity index 99% rename from src/agents/pi-tools.params.test.ts rename to src/agents/agent-tools.params.test.ts index 4fa624e73e5..efb5fc79864 100644 --- a/src/agents/pi-tools.params.test.ts +++ b/src/agents/agent-tools.params.test.ts @@ -4,7 +4,7 @@ import { REQUIRED_PARAM_GROUPS, getToolParamsRecord, wrapToolParamValidation, -} from "./pi-tools.params.js"; +} from "./agent-tools.params.js"; describe("assertRequiredParams", () => { it("returns object params unchanged", () => { diff --git a/src/agents/pi-tools.params.ts b/src/agents/agent-tools.params.ts similarity index 98% rename from src/agents/pi-tools.params.ts rename to src/agents/agent-tools.params.ts index bf34a878b90..1df63f75c6d 100644 --- a/src/agents/pi-tools.params.ts +++ b/src/agents/agent-tools.params.ts @@ -1,4 +1,4 @@ -import type { AnyAgentTool } from "./pi-tools.types.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; export type RequiredParamGroup = { keys: readonly string[]; diff --git a/src/agents/pi-tools.policy.test.ts b/src/agents/agent-tools.policy.test.ts similarity index 98% rename from src/agents/pi-tools.policy.test.ts rename to src/agents/agent-tools.policy.test.ts index d1ef532211a..ef8e783fbdf 100644 --- a/src/agents/pi-tools.policy.test.ts +++ b/src/agents/agent-tools.policy.test.ts @@ -13,8 +13,8 @@ import { resolveSubagentToolPolicy, resolveSubagentToolPolicyForSession, resolveTrustedGroupId, -} from "./pi-tools.policy.js"; -import { createStubTool } from "./test-helpers/pi-tool-stubs.js"; +} from "./agent-tools.policy.js"; +import { createStubTool } from "./test-helpers/agent-tool-stubs.js"; import { providerAliasCases } from "./test-helpers/provider-alias-cases.js"; vi.mock("../channels/plugins/session-conversation.js", () => ({ @@ -26,7 +26,7 @@ vi.mock("../channels/plugins/session-conversation.js", () => ({ }), })); -describe("pi-tools.policy", () => { +describe("agent-tools.policy", () => { it("treats * in allow as allow-all", () => { const tools = [createStubTool("read"), createStubTool("exec")]; const filtered = filterToolsByPolicy(tools, { allow: ["*"] }); @@ -752,7 +752,7 @@ describe("resolveEffectiveToolPolicy", () => { }); it("does not warn an agent profile about inherited global tool sections (#47487)", async () => { - const warnLogs = createWarnLogCapture("openclaw-pi-tools-policy-test"); + const warnLogs = createWarnLogCapture("openclaw-agent-tools-policy-test"); try { const cfg = { tools: { @@ -781,7 +781,7 @@ describe("resolveEffectiveToolPolicy", () => { }); it("still warns when an agent profile has its own configured exec section (#47487)", async () => { - const warnLogs = createWarnLogCapture("openclaw-pi-tools-policy-test"); + const warnLogs = createWarnLogCapture("openclaw-agent-tools-policy-test"); try { const cfg = { agents: { @@ -809,7 +809,7 @@ describe("resolveEffectiveToolPolicy", () => { }); it("only lists configured sections whose grants are still missing (#47487)", async () => { - const warnLogs = createWarnLogCapture("openclaw-pi-tools-policy-test"); + const warnLogs = createWarnLogCapture("openclaw-agent-tools-policy-test"); try { const cfg = { agents: { diff --git a/src/agents/pi-tools.policy.ts b/src/agents/agent-tools.policy.ts similarity index 99% rename from src/agents/pi-tools.policy.ts rename to src/agents/agent-tools.policy.ts index 3ac4ab9f84e..79787ced197 100644 --- a/src/agents/pi-tools.policy.ts +++ b/src/agents/agent-tools.policy.ts @@ -20,7 +20,7 @@ import { } from "../shared/string-normalization.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; import { resolveAgentConfig, resolveAgentIdFromSessionKey } from "./agent-scope.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; import { normalizeProviderId } from "./provider-id.js"; import { pickSandboxToolPolicy } from "./sandbox-tool-policy.js"; import type { SandboxToolPolicy } from "./sandbox.js"; diff --git a/src/agents/pi-tools.read.host-edit-access.test.ts b/src/agents/agent-tools.read.host-edit-access.test.ts similarity index 90% rename from src/agents/pi-tools.read.host-edit-access.test.ts rename to src/agents/agent-tools.read.host-edit-access.test.ts index dbc467c6a0f..607e5322802 100644 --- a/src/agents/pi-tools.read.host-edit-access.test.ts +++ b/src/agents/agent-tools.read.host-edit-access.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { createHostWorkspaceEditTool } from "./pi-tools.read.js"; +import { createHostWorkspaceEditTool } from "./agent-tools.read.js"; type CapturedEditOperations = { access: (absolutePath: string) => Promise; @@ -12,9 +12,9 @@ const mocks = vi.hoisted(() => ({ operations: undefined as CapturedEditOperations | undefined, })); -vi.mock("@earendil-works/pi-coding-agent", async () => { - const actual = await vi.importActual( - "@earendil-works/pi-coding-agent", +vi.mock("openclaw/plugin-sdk/agent-sessions", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/agent-sessions", ); return { ...actual, diff --git a/src/agents/pi-tools.read.host-edit-recovery.test.ts b/src/agents/agent-tools.read.host-edit-recovery.test.ts similarity index 99% rename from src/agents/pi-tools.read.host-edit-recovery.test.ts rename to src/agents/agent-tools.read.host-edit-recovery.test.ts index 8f4059321f4..fcaf63c7adb 100644 --- a/src/agents/pi-tools.read.host-edit-recovery.test.ts +++ b/src/agents/agent-tools.read.host-edit-recovery.test.ts @@ -2,10 +2,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { wrapEditToolWithRecovery, wrapWriteToolWithRecovery } from "./pi-tools.host-edit.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +import { wrapEditToolWithRecovery, wrapWriteToolWithRecovery } from "./agent-tools.host-edit.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; +import type { AgentToolResult } from "./runtime/index.js"; import type { SandboxFsBridge, SandboxFsStat } from "./sandbox/fs-bridge.js"; function createInMemoryBridge(root: string, files: Map): SandboxFsBridge { diff --git a/src/agents/pi-tools.read.host-tilde-expansion.test.ts b/src/agents/agent-tools.read.host-tilde-expansion.test.ts similarity index 96% rename from src/agents/pi-tools.read.host-tilde-expansion.test.ts rename to src/agents/agent-tools.read.host-tilde-expansion.test.ts index 441c16d43d3..c8e8b341987 100644 --- a/src/agents/pi-tools.read.host-tilde-expansion.test.ts +++ b/src/agents/agent-tools.read.host-tilde-expansion.test.ts @@ -19,9 +19,9 @@ const mocks = vi.hoisted(() => ({ writeOps: undefined as CapturedWriteOperations | undefined, })); -vi.mock("@earendil-works/pi-coding-agent", async () => { - const actual = await vi.importActual( - "@earendil-works/pi-coding-agent", +vi.mock("openclaw/plugin-sdk/agent-sessions", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/agent-sessions", ); return { ...actual, @@ -47,7 +47,7 @@ vi.mock("@earendil-works/pi-coding-agent", async () => { }); const { createHostWorkspaceEditTool, createHostWorkspaceWriteTool } = - await import("./pi-tools.read.js"); + await import("./agent-tools.read.js"); const osHome = () => process.env.HOME ?? os.homedir(); const toTildePath = (absolutePath: string) => absolutePath.replace(osHome(), "~"); diff --git a/src/agents/pi-tools.read.ts b/src/agents/agent-tools.read.ts similarity index 99% rename from src/agents/pi-tools.read.ts rename to src/agents/agent-tools.read.ts index 2a17db2db1c..d68a2f60402 100644 --- a/src/agents/pi-tools.read.ts +++ b/src/agents/agent-tools.read.ts @@ -1,8 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; import { URL } from "node:url"; -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; -import { createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-coding-agent"; import { isWindowsDrivePath } from "../infra/archive-path.js"; import { canonicalPathFromExistingAncestor, @@ -13,18 +11,20 @@ import { expandHomePrefix, resolveOsHomeDir } from "../infra/home-dir.js"; import { hasEncodedFileUrlSeparator, trySafeFileURLToPath } from "../infra/local-file-access.js"; import { detectMime } from "../media/mime.js"; import { sniffMimeFromBase64 } from "../media/sniff-mime-from-base64.js"; -import type { ImageSanitizationLimits } from "./image-sanitization.js"; -import { toRelativeWorkspacePath } from "./path-policy.js"; -import { wrapEditToolWithRecovery, wrapWriteToolWithRecovery } from "./pi-tools.host-edit.js"; +import { wrapEditToolWithRecovery, wrapWriteToolWithRecovery } from "./agent-tools.host-edit.js"; import { REQUIRED_PARAM_GROUPS, assertRequiredParams, getToolParamsRecord, wrapToolParamValidation, -} from "./pi-tools.params.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +} from "./agent-tools.params.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; +import type { ImageSanitizationLimits } from "./image-sanitization.js"; +import { toRelativeWorkspacePath } from "./path-policy.js"; +import type { AgentToolResult } from "./runtime/index.js"; import { assertSandboxPath } from "./sandbox-paths.js"; import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; +import { createEditTool, createReadTool, createWriteTool } from "./sessions/index.js"; import { sanitizeToolResultImages } from "./tool-images.js"; export { @@ -32,7 +32,7 @@ export { assertRequiredParams, getToolParamsRecord, wrapToolParamValidation, -} from "./pi-tools.params.js"; +} from "./agent-tools.params.js"; // NOTE(steipete): Upstream read now does file-magic MIME detection; we keep the wrapper // to sanitize oversized images before they hit providers. @@ -360,7 +360,7 @@ async function executeReadWithAdaptivePaging(params: { } function rewriteReadImageHeader(text: string, mimeType: string): string { - // pi-coding-agent uses: "Read image file [image/png]" + // session runtime uses: "Read image file [image/png]" if (text.startsWith("Read image file [") && text.endsWith("]")) { return `Read image file [${mimeType}]`; } diff --git a/src/agents/pi-tools.read.workspace-root-guard.test.ts b/src/agents/agent-tools.read.workspace-root-guard.test.ts similarity index 96% rename from src/agents/pi-tools.read.workspace-root-guard.test.ts rename to src/agents/agent-tools.read.workspace-root-guard.test.ts index 916c979d418..e0dd3b76701 100644 --- a/src/agents/pi-tools.read.workspace-root-guard.test.ts +++ b/src/agents/agent-tools.read.workspace-root-guard.test.ts @@ -1,6 +1,6 @@ import path from "node:path"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; type AssertSandboxPath = typeof import("./sandbox-paths.js").assertSandboxPath; @@ -29,10 +29,10 @@ function createToolHarness() { } async function loadModule() { - ({ wrapToolWorkspaceRootGuardWithOptions } = await import("./pi-tools.read.js")); + ({ wrapToolWorkspaceRootGuardWithOptions } = await import("./agent-tools.read.js")); } -let wrapToolWorkspaceRootGuardWithOptions: typeof import("./pi-tools.read.js").wrapToolWorkspaceRootGuardWithOptions; +let wrapToolWorkspaceRootGuardWithOptions: typeof import("./agent-tools.read.js").wrapToolWorkspaceRootGuardWithOptions; describe("wrapToolWorkspaceRootGuardWithOptions", () => { const root = "/tmp/root"; diff --git a/src/agents/pi-tools.safe-bins.test.ts b/src/agents/agent-tools.safe-bins.test.ts similarity index 98% rename from src/agents/pi-tools.safe-bins.test.ts rename to src/agents/agent-tools.safe-bins.test.ts index 6fc7314f4bf..18f29948e9f 100644 --- a/src/agents/pi-tools.safe-bins.test.ts +++ b/src/agents/agent-tools.safe-bins.test.ts @@ -8,7 +8,7 @@ import type { SafeBinProfileFixture } from "../infra/exec-safe-bin-policy.js"; import { withEnvAsync } from "../test-utils/env.js"; import { resetProcessRegistryForTests } from "./bash-process-registry.js"; -let createOpenClawCodingTools: typeof import("./pi-tools.js").createOpenClawCodingTools; +let createOpenClawCodingTools: typeof import("./agent-tools.js").createOpenClawCodingTools; const { mockExecApprovals, supervisorSpawnMock } = vi.hoisted(() => { const execApprovals = { @@ -78,7 +78,7 @@ beforeAll(async () => { OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(os.tmpdir(), "openclaw-test-no-bundled-extensions"), }, async () => { - ({ createOpenClawCodingTools } = await import("./pi-tools.js")); + ({ createOpenClawCodingTools } = await import("./agent-tools.js")); }, ); }); @@ -138,7 +138,7 @@ vi.mock("../plugins/tools.js", () => ({ getPluginToolMeta: () => undefined, })); -vi.mock("@earendil-works/pi-coding-agent", () => ({ +vi.mock("openclaw/plugin-sdk/agent-sessions", () => ({ AuthStorage: vi.fn(), CURRENT_SESSION_VERSION: 1, ModelRegistry: vi.fn(), diff --git a/src/agents/pi-tools.sandbox-mounted-paths.workspace-only.test.ts b/src/agents/agent-tools.sandbox-mounted-paths.workspace-only.test.ts similarity index 98% rename from src/agents/pi-tools.sandbox-mounted-paths.workspace-only.test.ts rename to src/agents/agent-tools.sandbox-mounted-paths.workspace-only.test.ts index 934b242ab9b..87f77c7f327 100644 --- a/src/agents/pi-tools.sandbox-mounted-paths.workspace-only.test.ts +++ b/src/agents/agent-tools.sandbox-mounted-paths.workspace-only.test.ts @@ -2,19 +2,19 @@ import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { createApplyPatchTool } from "./apply-patch.js"; import { createSandboxedEditTool, createSandboxedReadTool, createSandboxedWriteTool, wrapToolWorkspaceRootGuardWithOptions, -} from "./pi-tools.read.js"; +} from "./agent-tools.read.js"; +import { createApplyPatchTool } from "./apply-patch.js"; import { SANDBOX_AGENT_WORKSPACE_MOUNT } from "./sandbox/constants.js"; import { expectReadWriteEditTools, expectReadWriteTools, getTextContent, -} from "./test-helpers/pi-tools-fs-helpers.js"; +} from "./test-helpers/agent-tools-fs-helpers.js"; import { withUnsafeMountedSandboxHarness } from "./test-helpers/unsafe-mounted-sandbox.js"; vi.mock("../infra/shell-env.js", async () => { diff --git a/src/agents/pi-tools.schema.test.ts b/src/agents/agent-tools.schema.test.ts similarity index 99% rename from src/agents/pi-tools.schema.test.ts rename to src/agents/agent-tools.schema.test.ts index 2e8afefb0d0..48011a85a05 100644 --- a/src/agents/pi-tools.schema.test.ts +++ b/src/agents/agent-tools.schema.test.ts @@ -1,14 +1,14 @@ -import { runAgentLoop, type AgentEvent, type StreamFn } from "@earendil-works/pi-agent-core"; -import { createAssistantMessageEventStream, validateToolArguments } from "@earendil-works/pi-ai"; +import { runAgentLoop, type AgentEvent, type StreamFn } from "openclaw/plugin-sdk/agent-core"; +import { createAssistantMessageEventStream, validateToolArguments } from "openclaw/plugin-sdk/llm"; import { Type, type TSchema } from "typebox"; import { describe, expect, it, vi } from "vitest"; -import { wrapToolWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js"; +import { wrapToolWithBeforeToolCallHook } from "./agent-tools.before-tool-call.js"; import { cleanToolSchemaForGemini, normalizeToolParameterSchema, normalizeToolParameters, -} from "./pi-tools.schema.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +} from "./agent-tools.schema.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; const TEST_USAGE = { input: 0, diff --git a/src/agents/pi-tools.schema.ts b/src/agents/agent-tools.schema.ts similarity index 96% rename from src/agents/pi-tools.schema.ts rename to src/agents/agent-tools.schema.ts index 48bb3cd5549..0acfcaa48f9 100644 --- a/src/agents/pi-tools.schema.ts +++ b/src/agents/agent-tools.schema.ts @@ -1,10 +1,10 @@ import { copyPluginToolMeta } from "../plugins/tools.js"; -import { copyChannelAgentToolMeta } from "./channel-tools.js"; import { normalizeToolParameterSchema, type ToolParameterSchemaOptions, -} from "./pi-tools-parameter-schema.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +} from "./agent-tools-parameter-schema.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; +import { copyChannelAgentToolMeta } from "./channel-tools.js"; export { normalizeToolParameterSchema }; diff --git a/src/agents/pi-tools.ts b/src/agents/agent-tools.ts similarity index 98% rename from src/agents/pi-tools.ts rename to src/agents/agent-tools.ts index ea48bcb4d63..dbb45740688 100644 --- a/src/agents/pi-tools.ts +++ b/src/agents/agent-tools.ts @@ -1,5 +1,4 @@ import path from "node:path"; -import { createCodingTools, createReadTool } from "@earendil-works/pi-coding-agent"; import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js"; import { HEARTBEAT_RESPONSE_TOOL_NAME } from "../auto-reply/heartbeat-tool-response.js"; import type { InboundEventKind } from "../channels/inbound-event/kind.js"; @@ -18,33 +17,20 @@ import { } from "../shared/string-coerce.js"; import { resolveGatewayMessageChannel } from "../utils/message-channel.js"; import { resolveAgentConfig } from "./agent-scope.js"; -import { createApplyPatchTool } from "./apply-patch.js"; -import type { AuthProfileStore } from "./auth-profiles/types.js"; -import { describeExecTool, describeProcessTool } from "./bash-tools.descriptions.js"; -import type { ExecToolDefaults } from "./bash-tools.exec-types.js"; -import type { ProcessToolDefaults } from "./bash-tools.process.js"; -import { execSchema, processSchema } from "./bash-tools.schemas.js"; -import { listChannelAgentTools } from "./channel-tools.js"; -import { shouldSuppressManagedWebSearchTool } from "./codex-native-web-search.js"; -import { resolveImageSanitizationLimits } from "./image-sanitization.js"; -import { filterLocalModelLeanTools } from "./local-model-lean.js"; -import type { ModelAuthMode } from "./model-auth.js"; -import { resolveOpenClawPluginToolsForOptions } from "./openclaw-plugin-tools.js"; -import { createOpenClawTools } from "./openclaw-tools.js"; -import { wrapToolWithAbortSignal } from "./pi-tools.abort.js"; +import { wrapToolWithAbortSignal } from "./agent-tools.abort.js"; import { type ToolOutcomeObserver, wrapToolWithBeforeToolCallHook, -} from "./pi-tools.before-tool-call.js"; -import { applyDeferredFollowupToolDescriptions } from "./pi-tools.deferred-followup.js"; -import { filterToolsByMessageProvider } from "./pi-tools.message-provider-policy.js"; +} from "./agent-tools.before-tool-call.js"; +import { applyDeferredFollowupToolDescriptions } from "./agent-tools.deferred-followup.js"; +import { filterToolsByMessageProvider } from "./agent-tools.message-provider-policy.js"; import { isToolAllowedByPolicies, resolveEffectiveToolPolicy, resolveGroupToolPolicy, resolveInheritedToolPolicyForSession, resolveSubagentToolPolicyForSession, -} from "./pi-tools.policy.js"; +} from "./agent-tools.policy.js"; import { assertRequiredParams, createHostWorkspaceEditTool, @@ -58,12 +44,26 @@ import { wrapToolWorkspaceRootGuard, wrapToolWorkspaceRootGuardWithOptions, wrapToolParamValidation, -} from "./pi-tools.read.js"; -import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +} from "./agent-tools.read.js"; +import { cleanToolSchemaForGemini, normalizeToolParameters } from "./agent-tools.schema.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; +import { createApplyPatchTool } from "./apply-patch.js"; +import type { AuthProfileStore } from "./auth-profiles/types.js"; +import { describeExecTool, describeProcessTool } from "./bash-tools.descriptions.js"; +import type { ExecToolDefaults } from "./bash-tools.exec-types.js"; +import type { ProcessToolDefaults } from "./bash-tools.process.js"; +import { execSchema, processSchema } from "./bash-tools.schemas.js"; +import { listChannelAgentTools } from "./channel-tools.js"; +import { shouldSuppressManagedWebSearchTool } from "./codex-native-web-search.js"; +import { resolveImageSanitizationLimits } from "./image-sanitization.js"; +import { filterLocalModelLeanTools } from "./local-model-lean.js"; +import type { ModelAuthMode } from "./model-auth.js"; +import { resolveOpenClawPluginToolsForOptions } from "./openclaw-plugin-tools.js"; +import { createOpenClawTools } from "./openclaw-tools.js"; import type { SandboxContext } from "./sandbox.js"; import { SANDBOX_AGENT_WORKSPACE_MOUNT } from "./sandbox/constants.js"; import { resolveSenderToolPolicy } from "./sender-tool-policy.js"; +import { createCodingTools, createReadTool } from "./sessions/index.js"; import type { SkillSnapshot } from "./skills/types.js"; import { isSubagentEnvelopeSession, @@ -452,9 +452,9 @@ export function createOpenClawCodingTools(options?: { includeCoreTools?: boolean; /** Include Tool Search control tools when enabled for this run. */ includeToolSearchControls?: boolean; - /** Executes cataloged tools through the active PI run lifecycle. */ + /** Executes cataloged tools through the active agent run lifecycle. */ toolSearchCatalogExecutor?: ToolSearchCatalogToolExecutor; - /** Runtime-local Tool Search catalog ref shared with PI attempt compaction. */ + /** Runtime-local Tool Search catalog ref shared with attempt compaction. */ toolSearchCatalogRef?: ToolSearchCatalogRef; /** Limits which tool families are materialized before the shared policy pipeline runs. */ toolConstructionPlan?: OpenClawCodingToolConstructionPlan; @@ -1058,7 +1058,7 @@ export function createOpenClawCodingTools(options?: { replaceWithEffectiveToolAllowlist(inheritedToolAllowlist, subagentFiltered); } options?.recordToolPrepStage?.("authorization-policy"); - // Always normalize tool JSON Schemas before handing them to pi-agent/pi-ai. + // Always normalize tool JSON Schemas before handing them to OpenClaw model runtime. // Without this, some providers (notably OpenAI) will reject root-level union schemas. // Provider-specific cleaning: Gemini needs constraint keywords stripped, but Anthropic expects them. const normalized = subagentFiltered.map((tool) => @@ -1103,7 +1103,7 @@ export function createOpenClawCodingTools(options?: { options?.recordToolPrepStage?.("deferred-followup-descriptions"); // NOTE: Keep canonical (lowercase) tool names here. - // pi-ai's Anthropic OAuth transport remaps tool names to Claude Code-style names + // shared model runtime's Anthropic OAuth transport remaps tool names to Claude Code-style names // on the wire and maps them back for tool dispatch. return withDeferredFollowupDescriptions; } diff --git a/src/agents/pi-tools.types.ts b/src/agents/agent-tools.types.ts similarity index 100% rename from src/agents/pi-tools.types.ts rename to src/agents/agent-tools.types.ts diff --git a/src/agents/pi-tools.workspace-only-false.test.ts b/src/agents/agent-tools.workspace-only-false.test.ts similarity index 95% rename from src/agents/pi-tools.workspace-only-false.test.ts rename to src/agents/agent-tools.workspace-only-false.test.ts index 65320e4bd2a..5d63ebc7852 100644 --- a/src/agents/pi-tools.workspace-only-false.test.ts +++ b/src/agents/agent-tools.workspace-only-false.test.ts @@ -1,20 +1,20 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { createReadTool } from "@earendil-works/pi-coding-agent"; +import { createReadTool } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("@earendil-works/pi-ai", async () => { +vi.mock("openclaw/plugin-sdk/llm", async () => { const original = - await vi.importActual("@earendil-works/pi-ai"); + await vi.importActual("openclaw/plugin-sdk/llm"); return { ...original, }; }); -vi.mock("@earendil-works/pi-ai/oauth", async () => { - const actual = await vi.importActual( - "@earendil-works/pi-ai/oauth", +vi.mock("openclaw/plugin-sdk/llm-oauth", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/llm-oauth", ); return { ...actual, @@ -29,7 +29,7 @@ import { createOpenClawReadTool, wrapToolMemoryFlushAppendOnlyWrite, wrapToolWorkspaceRootGuard, -} from "./pi-tools.read.js"; +} from "./agent-tools.read.js"; import type { AnyAgentTool } from "./tools/common.js"; describe("FS tools with workspaceOnly=false", () => { diff --git a/src/agents/pi-tools.workspace-paths.test.ts b/src/agents/agent-tools.workspace-paths.test.ts similarity index 98% rename from src/agents/pi-tools.workspace-paths.test.ts rename to src/agents/agent-tools.workspace-paths.test.ts index 7ac34270272..645f9f1208f 100644 --- a/src/agents/pi-tools.workspace-paths.test.ts +++ b/src/agents/agent-tools.workspace-paths.test.ts @@ -5,11 +5,11 @@ import { describe, expect, it, vi } from "vitest"; import "./test-helpers/fast-coding-tools.js"; import "./test-helpers/fast-openclaw-tools.js"; import type { OpenClawConfig } from "../config/config.js"; -import { createOpenClawCodingTools } from "./pi-tools.js"; +import { createOpenClawCodingTools } from "./agent-tools.js"; import { createCanonicalFixtureSkill } from "./skills.test-helpers.js"; +import { expectReadWriteEditTools, getTextContent } from "./test-helpers/agent-tools-fs-helpers.js"; +import { createAgentToolsSandboxContext } from "./test-helpers/agent-tools-sandbox-context.js"; import { createHostSandboxFsBridge } from "./test-helpers/host-sandbox-fs-bridge.js"; -import { expectReadWriteEditTools, getTextContent } from "./test-helpers/pi-tools-fs-helpers.js"; -import { createPiToolsSandboxContext } from "./test-helpers/pi-tools-sandbox-context.js"; vi.mock("../infra/shell-env.js", async () => { const mod = @@ -435,7 +435,7 @@ describe("sandboxed workspace paths", () => { it("uses sandbox workspace for relative read/write/edit", async () => { await withTempDir("openclaw-sandbox-", async (sandboxDir) => { await withTempDir("openclaw-workspace-", async (workspaceDir) => { - const sandbox = createPiToolsSandboxContext({ + const sandbox = createAgentToolsSandboxContext({ workspaceDir: sandboxDir, agentWorkspaceDir: workspaceDir, workspaceAccess: "rw" as const, diff --git a/src/agents/anthropic-payload-log.test.ts b/src/agents/anthropic-payload-log.test.ts index c39a6cc8179..cb7d61db94b 100644 --- a/src/agents/anthropic-payload-log.test.ts +++ b/src/agents/anthropic-payload-log.test.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -import type { StreamFn } from "@earendil-works/pi-agent-core"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; import { createAnthropicPayloadLogger } from "./anthropic-payload-log.js"; diff --git a/src/agents/anthropic-payload-log.ts b/src/agents/anthropic-payload-log.ts index a1207bf73b4..fcee1dc38b8 100644 --- a/src/agents/anthropic-payload-log.ts +++ b/src/agents/anthropic-payload-log.ts @@ -1,7 +1,6 @@ import crypto from "node:crypto"; import path from "node:path"; -import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core"; -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { resolveStateDir } from "../config/paths.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { resolveUserPath } from "../utils.js"; @@ -9,6 +8,7 @@ import { parseBooleanValue } from "../utils/boolean.js"; import { safeJsonStringify } from "../utils/safe-json.js"; import { sanitizeDiagnosticPayload } from "./payload-redaction.js"; import { getQueuedFileWriter, type QueuedFileWriter } from "./queued-file-writer.js"; +import type { AgentMessage, StreamFn } from "./runtime/index.js"; type PayloadLogStage = "request" | "usage"; @@ -77,7 +77,7 @@ function digest(value: unknown): string | undefined { return crypto.createHash("sha256").update(serialized).digest("hex"); } -function isAnthropicModel(model: Model | undefined | null): boolean { +function isAnthropicModel(model: Model | undefined | null): boolean { return (model as { api?: unknown })?.api === "anthropic-messages"; } diff --git a/src/agents/anthropic-payload-policy.test.ts b/src/agents/anthropic-payload-policy.test.ts index 4650cc9a8da..47ce7cd0ff3 100644 --- a/src/agents/anthropic-payload-policy.test.ts +++ b/src/agents/anthropic-payload-policy.test.ts @@ -123,8 +123,8 @@ describe("anthropic payload policy", () => { }); it("keeps implicit env-driven long retention conservative for custom hosts", () => { - const previous = process.env.PI_CACHE_RETENTION; - process.env.PI_CACHE_RETENTION = "long"; + const previous = process.env.OPENCLAW_CACHE_RETENTION; + process.env.OPENCLAW_CACHE_RETENTION = "long"; try { const policy = resolveAnthropicPayloadPolicy({ provider: "anthropic", @@ -139,9 +139,9 @@ describe("anthropic payload policy", () => { expectShortEphemeralTextPayload(payload); } finally { if (previous === undefined) { - delete process.env.PI_CACHE_RETENTION; + delete process.env.OPENCLAW_CACHE_RETENTION; } else { - process.env.PI_CACHE_RETENTION = previous; + process.env.OPENCLAW_CACHE_RETENTION = previous; } } }); diff --git a/src/agents/anthropic-payload-policy.ts b/src/agents/anthropic-payload-policy.ts index 665a3343237..e52dd63bdcc 100644 --- a/src/agents/anthropic-payload-policy.ts +++ b/src/agents/anthropic-payload-policy.ts @@ -57,7 +57,7 @@ function resolveAnthropicEphemeralCacheControl( cacheRetention: AnthropicPayloadPolicyInput["cacheRetention"], ): AnthropicEphemeralCacheControl | undefined { const retention = - cacheRetention ?? (process.env.PI_CACHE_RETENTION === "long" ? "long" : "short"); + cacheRetention ?? (process.env.OPENCLAW_CACHE_RETENTION === "long" ? "long" : "short"); if (retention === "none") { return undefined; } diff --git a/src/agents/anthropic-transport-stream.live.test.ts b/src/agents/anthropic-transport-stream.live.test.ts index ebb52664290..a816be2e2e4 100644 --- a/src/agents/anthropic-transport-stream.live.test.ts +++ b/src/agents/anthropic-transport-stream.live.test.ts @@ -1,5 +1,5 @@ import http from "node:http"; -import type { Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { createAnthropicMessagesTransportStreamFn } from "./anthropic-transport-stream.js"; import { isLiveTestEnabled } from "./live-test-helpers.js"; diff --git a/src/agents/anthropic-transport-stream.test.ts b/src/agents/anthropic-transport-stream.test.ts index 5da1853bf72..40dd00b70c7 100644 --- a/src/agents/anthropic-transport-stream.test.ts +++ b/src/agents/anthropic-transport-stream.test.ts @@ -1,4 +1,4 @@ -import type { Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { attachModelProviderRequestTransport } from "./provider-request-config.js"; diff --git a/src/agents/anthropic-transport-stream.ts b/src/agents/anthropic-transport-stream.ts index b9f1c476baf..202e01ce51f 100644 --- a/src/agents/anthropic-transport-stream.ts +++ b/src/agents/anthropic-transport-stream.ts @@ -1,4 +1,3 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; import { calculateCost, getEnvApiKey, @@ -8,7 +7,7 @@ import { type Model, type SimpleStreamOptions, type ThinkingLevel, -} from "@earendil-works/pi-ai"; +} from "openclaw/plugin-sdk/llm"; import { MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE } from "../shared/assistant-error-format.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { @@ -19,6 +18,7 @@ import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./copilot-dyn import { parseJsonObjectPreservingUnsafeIntegers } from "./json-unsafe-integers.js"; import { resolveProviderEndpoint } from "./provider-attribution.js"; import { buildGuardedModelFetch } from "./provider-transport-fetch.js"; +import type { StreamFn } from "./runtime/index.js"; import { transformTransportMessages } from "./transport-message-transform.js"; import { coerceTransportToolCallArguments, diff --git a/src/agents/anthropic-vertex-stream.ts b/src/agents/anthropic-vertex-stream.ts index c1fc2263bab..30bd21eca67 100644 --- a/src/agents/anthropic-vertex-stream.ts +++ b/src/agents/anthropic-vertex-stream.ts @@ -1,5 +1,5 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; import { loadBundledPluginPublicSurfaceModuleSync } from "../plugin-sdk/facade-runtime.js"; +import type { StreamFn } from "./runtime/index.js"; type AnthropicVertexStreamFacade = { createAnthropicVertexStreamFn: ( diff --git a/src/agents/anthropic.setup-token.live.test.ts b/src/agents/anthropic.setup-token.live.test.ts index 429d9151aaf..73a61eb5cc0 100644 --- a/src/agents/anthropic.setup-token.live.test.ts +++ b/src/agents/anthropic.setup-token.live.test.ts @@ -2,13 +2,14 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { type Api, completeSimple, type Model } from "@earendil-works/pi-ai"; +import { type Api, completeSimple, type Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { ANTHROPIC_SETUP_TOKEN_PREFIX, validateAnthropicSetupToken, } from "../commands/auth-token.js"; import { getRuntimeConfig } from "../config/config.js"; +import { discoverAuthStorage, discoverModels } from "./agent-model-discovery.js"; import { resolveDefaultAgentDir } from "./agent-scope.js"; import { type AuthProfileCredential, @@ -19,7 +20,6 @@ import { isLiveTestEnabled } from "./live-test-helpers.js"; import { getApiKeyForModel, requireApiKey } from "./model-auth.js"; import { normalizeProviderId, parseModelRef } from "./model-selection.js"; import { ensureOpenClawModelsJson } from "./models-config.js"; -import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js"; const LIVE = isLiveTestEnabled(); const SETUP_TOKEN_RAW = process.env.OPENCLAW_LIVE_SETUP_TOKEN?.trim() ?? ""; @@ -125,7 +125,7 @@ async function resolveTokenSource(): Promise { return { agentDir, profileId: pickSetupTokenProfile(candidates) }; } -function pickModel(models: Array>, raw?: string): Model | null { +function pickModel(models: Array, raw?: string): Model | null { const normalized = raw?.trim() ?? ""; if (normalized) { const parsed = parseModelRef(normalized, "anthropic"); @@ -156,8 +156,8 @@ function pickModel(models: Array>, raw?: string): Model | null { return models[0] ?? null; } -function buildTestModel(id: string, provider = "anthropic"): Model { - return { id, provider } as Model; +function buildTestModel(id: string, provider = "anthropic"): Model { + return { id, provider } as Model; } describe("pickModel", () => { @@ -192,7 +192,7 @@ describeLive("live anthropic setup-token", () => { const all = Array.isArray(modelRegistry) ? modelRegistry : modelRegistry.getAll(); const candidates = all.filter( (model) => normalizeProviderId(model.provider) === "anthropic", - ) as Array>; + ) as Array; expect(candidates.length).toBeGreaterThan(0); const model = pickModel(candidates, SETUP_TOKEN_MODEL); diff --git a/src/agents/apply-patch.ts b/src/agents/apply-patch.ts index ab48b2769cc..0b1b2730945 100644 --- a/src/agents/apply-patch.ts +++ b/src/agents/apply-patch.ts @@ -1,13 +1,13 @@ import syncFs from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; -import type { AgentTool } from "@earendil-works/pi-agent-core"; import { Type } from "typebox"; import { openRootFile, type RootFileOpenResult } from "../infra/boundary-file-read.js"; import { root as fsRoot } from "../infra/fs-safe.js"; import { PATH_ALIAS_POLICIES, type PathAliasPolicy } from "../infra/path-alias-guards.js"; import { applyUpdateHunk } from "./apply-patch-update.js"; import { toRelativeSandboxPath, resolvePathFromInput } from "./path-policy.js"; +import type { AgentTool } from "./runtime/index.js"; import { assertSandboxPath } from "./sandbox-paths.js"; import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; diff --git a/src/agents/auth-profile-runtime-contract.test.ts b/src/agents/auth-profile-runtime-contract.test.ts index ccc2e2373eb..15c647edd0b 100644 --- a/src/agents/auth-profile-runtime-contract.test.ts +++ b/src/agents/auth-profile-runtime-contract.test.ts @@ -11,8 +11,8 @@ import type { SessionEntry } from "../config/sessions.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type * as ManifestRegistryModule from "../plugins/manifest-registry.js"; import { runAgentAttempt } from "./command/attempt-execution.js"; -import type { RunEmbeddedPiAgentParams } from "./pi-embedded-runner/run/params.js"; -import type { EmbeddedPiRunResult } from "./pi-embedded.js"; +import type { RunEmbeddedAgentParams } from "./embedded-agent-runner/run/params.js"; +import type { EmbeddedAgentRunResult } from "./embedded-agent.js"; import { resolveProviderIdForAuth } from "./provider-auth-aliases.js"; type LoadPluginManifestRegistry = typeof ManifestRegistryModule.loadPluginManifestRegistry; @@ -24,7 +24,7 @@ const loadPluginManifestRegistry = vi.hoisted(() => })), ); const runCliAgentMock = vi.hoisted(() => vi.fn()); -const runEmbeddedPiAgentMock = vi.hoisted(() => vi.fn()); +const runEmbeddedAgentMock = vi.hoisted(() => vi.fn()); vi.mock("../plugins/manifest-registry.js", async (importOriginal) => { const actual = await importOriginal(); @@ -65,8 +65,8 @@ vi.mock("./model-selection.js", () => ({ normalizeProviderId: (provider: string) => provider.trim().toLowerCase(), })); -vi.mock("./pi-embedded.js", () => ({ - runEmbeddedPiAgent: runEmbeddedPiAgentMock, +vi.mock("./embedded-agent.js", () => ({ + runEmbeddedAgent: runEmbeddedAgentMock, })); function mockCallArg( @@ -85,12 +85,12 @@ function capturedCliRunParams(): { authProfileId?: string } { return mockCallArg(runCliAgentMock) as { authProfileId?: string }; } -function capturedEmbeddedRunParams(): RunEmbeddedPiAgentParams { - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); - return mockCallArg(runEmbeddedPiAgentMock) as RunEmbeddedPiAgentParams; +function capturedEmbeddedRunParams(): RunEmbeddedAgentParams { + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); + return mockCallArg(runEmbeddedAgentMock) as RunEmbeddedAgentParams; } -function makeCliResult(text: string): EmbeddedPiRunResult { +function makeCliResult(text: string): EmbeddedAgentRunResult { return { payloads: [{ text }], meta: { @@ -118,7 +118,7 @@ function makeCliResult(text: string): EmbeddedPiRunResult { }; } -function makeEmbeddedResult(text: string): EmbeddedPiRunResult { +function makeEmbeddedResult(text: string): EmbeddedAgentRunResult { return { payloads: [{ text }], meta: { @@ -219,7 +219,7 @@ async function runAuthContractAttempt(params: { }; } -describe("Auth profile runtime contract - Pi and CLI adapter", () => { +describe("Auth profile runtime contract - embedded OpenClaw and CLI adapter", () => { let tmpDir: string; let storePath: string; @@ -228,9 +228,9 @@ describe("Auth profile runtime contract - Pi and CLI adapter", () => { storePath = path.join(tmpDir, "sessions.json"); loadPluginManifestRegistry.mockReset().mockReturnValue(createAuthAliasManifestRegistry()); runCliAgentMock.mockReset(); - runEmbeddedPiAgentMock.mockReset(); + runEmbeddedAgentMock.mockReset(); runCliAgentMock.mockResolvedValue(makeCliResult("ok")); - runEmbeddedPiAgentMock.mockResolvedValue(makeEmbeddedResult("ok")); + runEmbeddedAgentMock.mockResolvedValue(makeEmbeddedResult("ok")); }); afterEach(async () => { @@ -348,7 +348,7 @@ describe("Auth profile runtime contract - Pi and CLI adapter", () => { expect(capturedCliRunParams().authProfileId).toBeUndefined(); }); - it("forwards an OpenAI Codex auth profile through the embedded Pi path", async () => { + it("forwards an OpenAI Codex auth profile through the embedded OpenClaw path", async () => { await runAuthContractAttempt({ tmpDir, storePath, @@ -381,14 +381,14 @@ describe("Auth profile runtime contract - Pi and CLI adapter", () => { ); }); - it("forwards an OpenAI auth profile through the explicit embedded OpenAI PI path", async () => { + it("forwards an OpenAI auth profile through the explicit embedded OpenAI OpenClaw path", async () => { await runAuthContractAttempt({ tmpDir, storePath, providerOverride: AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, authProfileProvider: AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, authProfileOverride: AUTH_PROFILE_RUNTIME_CONTRACT.openAiProfileId, - cfg: providerRuntimeConfig(AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, "pi"), + cfg: providerRuntimeConfig(AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, "openclaw"), }); const params = capturedEmbeddedRunParams(); @@ -410,14 +410,14 @@ describe("Auth profile runtime contract - Pi and CLI adapter", () => { ); }); - it("routes explicit OpenAI PI runs with Codex OAuth through OpenAI Codex transport", async () => { + it("routes explicit OpenAI OpenClaw runs with Codex OAuth through OpenAI Codex transport", async () => { await runAuthContractAttempt({ tmpDir, storePath, providerOverride: AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, authProfileProvider: AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProvider, authProfileOverride: AUTH_PROFILE_RUNTIME_CONTRACT.openAiCodexProfileId, - cfg: providerRuntimeConfig(AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, "pi"), + cfg: providerRuntimeConfig(AUTH_PROFILE_RUNTIME_CONTRACT.openAiProvider, "openclaw"), }); const params = capturedEmbeddedRunParams(); diff --git a/src/agents/auth-profiles.ensureauthprofilestore.test.ts b/src/agents/auth-profiles.ensureauthprofilestore.test.ts index c2d1a20475e..e101f304ccd 100644 --- a/src/agents/auth-profiles.ensureauthprofilestore.test.ts +++ b/src/agents/auth-profiles.ensureauthprofilestore.test.ts @@ -109,13 +109,11 @@ describe("ensureAuthProfileStore", () => { function restoreAgentDirEnv(params: { previousStateDir?: string | undefined; previousAgentDir: string | undefined; - previousPiAgentDir: string | undefined; }): void { if ("previousStateDir" in params) { restoreEnvValue("OPENCLAW_STATE_DIR", params.previousStateDir); } restoreEnvValue("OPENCLAW_AGENT_DIR", params.previousAgentDir); - restoreEnvValue("PI_CODING_AGENT_DIR", params.previousPiAgentDir); } function configureMainAuthTestDirs(root: string): { @@ -123,11 +121,9 @@ describe("ensureAuthProfileStore", () => { agentDir: string; previousStateDir: string | undefined; previousAgentDir: string | undefined; - previousPiAgentDir: string | undefined; } { const previousStateDir = process.env.OPENCLAW_STATE_DIR; const previousAgentDir = process.env.OPENCLAW_AGENT_DIR; - const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; const mainDir = path.join(root, "agents", "main", "agent"); const agentDir = path.join(root, "agents", "agent-x", "agent"); fs.mkdirSync(mainDir, { recursive: true }); @@ -135,9 +131,8 @@ describe("ensureAuthProfileStore", () => { process.env.OPENCLAW_STATE_DIR = root; process.env.OPENCLAW_AGENT_DIR = mainDir; - process.env.PI_CODING_AGENT_DIR = mainDir; clearRuntimeAuthProfileStoreSnapshots(); - return { mainDir, agentDir, previousStateDir, previousAgentDir, previousPiAgentDir }; + return { mainDir, agentDir, previousStateDir, previousAgentDir }; } function expectApiKeyProfile( @@ -251,7 +246,7 @@ describe("ensureAuthProfileStore", () => { it("merges main auth profiles into agent store and keeps agent overrides", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-merge-")); - const { mainDir, agentDir, previousStateDir, previousAgentDir, previousPiAgentDir } = + const { mainDir, agentDir, previousStateDir, previousAgentDir } = configureMainAuthTestDirs(root); try { const mainStore = { @@ -303,14 +298,14 @@ describe("ensureAuthProfileStore", () => { key: "agent-key", }); } finally { - restoreAgentDirEnv({ previousStateDir, previousAgentDir, previousPiAgentDir }); + restoreAgentDirEnv({ previousStateDir, previousAgentDir }); fs.rmSync(root, { recursive: true, force: true }); } }); it("uses the main agent's newer OAuth profile when an agent still has a stale default profile", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-drift-")); - const { mainDir, agentDir, previousStateDir, previousAgentDir, previousPiAgentDir } = + const { mainDir, agentDir, previousStateDir, previousAgentDir } = configureMainAuthTestDirs(root); try { const freshProfileId = "openai-codex:user@example.com"; @@ -393,14 +388,14 @@ describe("ensureAuthProfileStore", () => { ) as { profiles: Record }; expect(persistedAgentStore.profiles).toHaveProperty(staleProfileId); } finally { - restoreAgentDirEnv({ previousStateDir, previousAgentDir, previousPiAgentDir }); + restoreAgentDirEnv({ previousStateDir, previousAgentDir }); fs.rmSync(root, { recursive: true, force: true }); } }); it("keeps a newer agent replacement credential while repairing stale default references", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-drift-newer-agent-")); - const { mainDir, agentDir, previousStateDir, previousAgentDir, previousPiAgentDir } = + const { mainDir, agentDir, previousStateDir, previousAgentDir } = configureMainAuthTestDirs(root); try { const freshProfileId = "openai-codex:user@example.com"; @@ -468,14 +463,14 @@ describe("ensureAuthProfileStore", () => { expect(store.order?.["openai-codex"]).toEqual([freshProfileId]); expect(store.lastGood?.["openai-codex"]).toBe(freshProfileId); } finally { - restoreAgentDirEnv({ previousStateDir, previousAgentDir, previousPiAgentDir }); + restoreAgentDirEnv({ previousStateDir, previousAgentDir }); fs.rmSync(root, { recursive: true, force: true }); } }); it("preserves a valid main default OAuth profile while replacing a stale agent override", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-drift-base-default-")); - const { mainDir, agentDir, previousStateDir, previousAgentDir, previousPiAgentDir } = + const { mainDir, agentDir, previousStateDir, previousAgentDir } = configureMainAuthTestDirs(root); try { const freshProfileId = "openai-codex:user@example.com"; @@ -549,14 +544,14 @@ describe("ensureAuthProfileStore", () => { lastUsed: 123, }); } finally { - restoreAgentDirEnv({ previousStateDir, previousAgentDir, previousPiAgentDir }); + restoreAgentDirEnv({ previousStateDir, previousAgentDir }); fs.rmSync(root, { recursive: true, force: true }); } }); it("keeps a stale default OAuth profile when the main profile belongs to a different identity", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-drift-mismatch-")); - const { mainDir, agentDir, previousStateDir, previousAgentDir, previousPiAgentDir } = + const { mainDir, agentDir, previousStateDir, previousAgentDir } = configureMainAuthTestDirs(root); try { const freshProfileId = "openai-codex:user@example.com"; @@ -612,14 +607,14 @@ describe("ensureAuthProfileStore", () => { expect(store.order?.["openai-codex"]).toEqual([staleProfileId]); expect(store.lastGood?.["openai-codex"]).toBe(staleProfileId); } finally { - restoreAgentDirEnv({ previousStateDir, previousAgentDir, previousPiAgentDir }); + restoreAgentDirEnv({ previousStateDir, previousAgentDir }); fs.rmSync(root, { recursive: true, force: true }); } }); it("rewrites invalidated per-agent Codex order to the main agent's healthy relogin profile", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-codex-relogin-")); - const { mainDir, agentDir, previousStateDir, previousAgentDir, previousPiAgentDir } = + const { mainDir, agentDir, previousStateDir, previousAgentDir } = configureMainAuthTestDirs(root); try { const now = Date.now(); @@ -692,7 +687,7 @@ describe("ensureAuthProfileStore", () => { expect(store.lastGood?.["openai-codex"]).toBe(healthyProfileId); expect(store.usageStats?.[staleProfileId]).toBeUndefined(); } finally { - restoreAgentDirEnv({ previousStateDir, previousAgentDir, previousPiAgentDir }); + restoreAgentDirEnv({ previousStateDir, previousAgentDir }); fs.rmSync(root, { recursive: true, force: true }); } }); @@ -822,7 +817,6 @@ describe("ensureAuthProfileStore", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-oauth-migrate-")); const previousStateDir = process.env.OPENCLAW_STATE_DIR; const previousAgentDir = process.env.OPENCLAW_AGENT_DIR; - const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; try { const agentDir = path.join(root, "agent"); const oauthDir = path.join(root, "credentials"); @@ -847,7 +841,6 @@ describe("ensureAuthProfileStore", () => { process.env.OPENCLAW_STATE_DIR = root; process.env.OPENCLAW_AGENT_DIR = agentDir; - process.env.PI_CODING_AGENT_DIR = agentDir; clearRuntimeAuthProfileStoreSnapshots(); const store = ensureAuthProfileStore(agentDir); @@ -873,7 +866,7 @@ describe("ensureAuthProfileStore", () => { } finally { clearRuntimeAuthProfileStoreSnapshots(); restoreEnvValue("OPENCLAW_STATE_DIR", previousStateDir); - restoreAgentDirEnv({ previousAgentDir, previousPiAgentDir }); + restoreAgentDirEnv({ previousAgentDir }); fs.rmSync(root, { recursive: true, force: true }); } }); @@ -881,7 +874,6 @@ describe("ensureAuthProfileStore", () => { it("exposes provider-managed runtime auth without persisting copied tokens", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-external-auth-")); const previousAgentDir = process.env.OPENCLAW_AGENT_DIR; - const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; try { const agentDir = path.join(root, "agent"); fs.mkdirSync(agentDir, { recursive: true }); @@ -901,7 +893,6 @@ describe("ensureAuthProfileStore", () => { ]); process.env.OPENCLAW_AGENT_DIR = agentDir; - process.env.PI_CODING_AGENT_DIR = agentDir; clearRuntimeAuthProfileStoreSnapshots(); const store = ensureAuthProfileStore(agentDir); @@ -915,7 +906,7 @@ describe("ensureAuthProfileStore", () => { expect(fs.existsSync(path.join(agentDir, "auth-profiles.json"))).toBe(false); } finally { clearRuntimeAuthProfileStoreSnapshots(); - restoreAgentDirEnv({ previousAgentDir, previousPiAgentDir }); + restoreAgentDirEnv({ previousAgentDir }); fs.rmSync(root, { recursive: true, force: true }); } }); diff --git a/src/agents/auth-profiles.store-cache.test.ts b/src/agents/auth-profiles.store-cache.test.ts index ca85d6d8754..8014c6f68a4 100644 --- a/src/agents/auth-profiles.store-cache.test.ts +++ b/src/agents/auth-profiles.store-cache.test.ts @@ -33,10 +33,8 @@ vi.mock("../plugins/provider-runtime.js", () => ({ async function withAgentDirEnv(prefix: string, run: (agentDir: string) => void | Promise) { const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); const previousAgentDir = process.env.OPENCLAW_AGENT_DIR; - const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; try { process.env.OPENCLAW_AGENT_DIR = agentDir; - process.env.PI_CODING_AGENT_DIR = agentDir; await run(agentDir); } finally { if (previousAgentDir === undefined) { @@ -44,11 +42,6 @@ async function withAgentDirEnv(prefix: string, run: (agentDir: string) => void | } else { process.env.OPENCLAW_AGENT_DIR = previousAgentDir; } - if (previousPiAgentDir === undefined) { - delete process.env.PI_CODING_AGENT_DIR; - } else { - process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; - } fs.rmSync(agentDir, { recursive: true, force: true }); } } diff --git a/src/agents/auth-profiles/oauth-manager.test.ts b/src/agents/auth-profiles/oauth-manager.test.ts index 18106697931..8e0b215da71 100644 --- a/src/agents/auth-profiles/oauth-manager.test.ts +++ b/src/agents/auth-profiles/oauth-manager.test.ts @@ -39,7 +39,6 @@ const tempDirs: string[] = []; const envSnapshot = captureEnv([ "OPENCLAW_STATE_DIR", "OPENCLAW_AGENT_DIR", - "PI_CODING_AGENT_DIR", "OPENCLAW_OAUTH_DIR", "OPENCLAW_AUTH_PROFILE_SECRET_KEY", ]); @@ -313,7 +312,6 @@ describe("createOAuthManager", () => { const mainAgentDir = path.join(tempRoot, "agents", "main", "agent"); const agentDir = path.join(tempRoot, "agents", "sub", "agent"); process.env.OPENCLAW_AGENT_DIR = mainAgentDir; - process.env.PI_CODING_AGENT_DIR = mainAgentDir; await fs.mkdir(agentDir, { recursive: true }); await fs.mkdir(mainAgentDir, { recursive: true }); @@ -400,7 +398,6 @@ describe("createOAuthManager", () => { const mainAgentDir = path.join(tempRoot, "agents", "main", "agent"); const agentDir = path.join(tempRoot, "agents", "sub", "agent"); process.env.OPENCLAW_AGENT_DIR = mainAgentDir; - process.env.PI_CODING_AGENT_DIR = mainAgentDir; await fs.mkdir(agentDir, { recursive: true }); await fs.mkdir(mainAgentDir, { recursive: true }); const profileId = "minimax-portal:default"; diff --git a/src/agents/auth-profiles/oauth-refresh-queue.test.ts b/src/agents/auth-profiles/oauth-refresh-queue.test.ts index d95c8b1c85b..db3a9e05fe8 100644 --- a/src/agents/auth-profiles/oauth-refresh-queue.test.ts +++ b/src/agents/auth-profiles/oauth-refresh-queue.test.ts @@ -26,7 +26,7 @@ const { formatProviderAuthProfileApiKeyWithPluginMock, } = getOAuthProviderRuntimeMocks(); -vi.mock("@earendil-works/pi-ai/oauth", () => ({ +vi.mock("openclaw/plugin-sdk/llm-oauth", () => ({ getOAuthApiKey: vi.fn(async () => null), getOAuthProviders: () => [{ id: "openai-codex" }], })); diff --git a/src/agents/auth-profiles/oauth-test-utils.ts b/src/agents/auth-profiles/oauth-test-utils.ts index cbed1f4fc46..c3fb457d309 100644 --- a/src/agents/auth-profiles/oauth-test-utils.ts +++ b/src/agents/auth-profiles/oauth-test-utils.ts @@ -4,11 +4,7 @@ import path from "node:path"; import type { resolveApiKeyForProfile } from "./oauth.js"; import type { AuthProfileStore, OAuthCredential } from "./types.js"; -export const OAUTH_AGENT_ENV_KEYS = [ - "OPENCLAW_STATE_DIR", - "OPENCLAW_AGENT_DIR", - "PI_CODING_AGENT_DIR", -]; +export const OAUTH_AGENT_ENV_KEYS = ["OPENCLAW_STATE_DIR", "OPENCLAW_AGENT_DIR"]; export function resolveApiKeyForProfileInTest( resolver: typeof resolveApiKeyForProfile, @@ -64,7 +60,6 @@ export async function createOAuthMainAgentDir(stateDir: string): Promise const agentDir = path.join(stateDir, "agents", "main", "agent"); process.env.OPENCLAW_STATE_DIR = stateDir; process.env.OPENCLAW_AGENT_DIR = agentDir; - process.env.PI_CODING_AGENT_DIR = agentDir; await fs.mkdir(agentDir, { recursive: true }); return agentDir; } diff --git a/src/agents/auth-profiles/oauth.adopt-identity.test.ts b/src/agents/auth-profiles/oauth.adopt-identity.test.ts index 4f7b45c7749..35c03aca285 100644 --- a/src/agents/auth-profiles/oauth.adopt-identity.test.ts +++ b/src/agents/auth-profiles/oauth.adopt-identity.test.ts @@ -45,7 +45,7 @@ function expectPersistedOpenAICodexProfile( // sub-agent store. Unit tests cover policy variants; this suite proves each // production branch refuses a mismatched accountId. -vi.mock("@earendil-works/pi-ai/oauth", () => ({ +vi.mock("openclaw/plugin-sdk/llm-oauth", () => ({ getOAuthApiKey: vi.fn(async () => null), getOAuthProviders: () => [{ id: "openai-codex" }, { id: "anthropic" }], })); diff --git a/src/agents/auth-profiles/oauth.concurrent-agents.test.ts b/src/agents/auth-profiles/oauth.concurrent-agents.test.ts index c93a77db6db..447cbb48874 100644 --- a/src/agents/auth-profiles/oauth.concurrent-agents.test.ts +++ b/src/agents/auth-profiles/oauth.concurrent-agents.test.ts @@ -32,7 +32,7 @@ async function loadOAuthModuleForTest() { ({ resolveApiKeyForProfile, resetOAuthRefreshQueuesForTest } = await import("./oauth.js")); } -vi.mock("@earendil-works/pi-ai/oauth", () => ({ +vi.mock("openclaw/plugin-sdk/llm-oauth", () => ({ getOAuthApiKey: vi.fn(async () => null), getOAuthProviders: () => [{ id: "openai-codex" }], })); diff --git a/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts b/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts index 436016a4bcb..93fb592dbb6 100644 --- a/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts +++ b/src/agents/auth-profiles/oauth.fallback-to-main-agent.test.ts @@ -13,7 +13,7 @@ const { getOAuthApiKeyMock } = vi.hoisted(() => ({ }), })); -vi.mock("@earendil-works/pi-ai/oauth", () => ({ +vi.mock("openclaw/plugin-sdk/llm-oauth", () => ({ getOAuthApiKey: getOAuthApiKeyMock, getOAuthProviders: () => [{ id: "anthropic" }, { id: "openai-codex" }], })); @@ -37,7 +37,7 @@ vi.mock("../../plugins/provider-runtime.js", () => ({ })); afterAll(() => { - vi.doUnmock("@earendil-works/pi-ai/oauth"); + vi.doUnmock("openclaw/plugin-sdk/llm-oauth"); vi.doUnmock("../cli-credentials.js"); vi.doUnmock("../../plugins/provider-runtime.runtime.js"); vi.doUnmock("../../plugins/provider-runtime.js"); @@ -48,11 +48,7 @@ function createUsableOAuthExpiry(): number { } describe("resolveApiKeyForProfile fallback to main agent", () => { - const envSnapshot = captureEnv([ - "OPENCLAW_STATE_DIR", - "OPENCLAW_AGENT_DIR", - "PI_CODING_AGENT_DIR", - ]); + const envSnapshot = captureEnv(["OPENCLAW_STATE_DIR", "OPENCLAW_AGENT_DIR"]); let tmpDir: string; let mainAgentDir: string; let secondaryAgentDir: string; @@ -72,7 +68,6 @@ describe("resolveApiKeyForProfile fallback to main agent", () => { // Set environment variables so the default agent dir resolves under tmpDir. process.env.OPENCLAW_STATE_DIR = tmpDir; process.env.OPENCLAW_AGENT_DIR = mainAgentDir; - process.env.PI_CODING_AGENT_DIR = mainAgentDir; clearRuntimeAuthProfileStoreSnapshots(); }); diff --git a/src/agents/auth-profiles/oauth.mirror-refresh.test.ts b/src/agents/auth-profiles/oauth.mirror-refresh.test.ts index 17057df1716..7cf0097215d 100644 --- a/src/agents/auth-profiles/oauth.mirror-refresh.test.ts +++ b/src/agents/auth-profiles/oauth.mirror-refresh.test.ts @@ -47,7 +47,7 @@ function requireOAuthCredential(store: AuthProfileStore, profileId: string): OAu return profile; } -vi.mock("@mariozechner/pi-ai/oauth", () => ({ +vi.mock("openclaw/plugin-sdk/llm-oauth", () => ({ getOAuthProviders: () => [{ id: "anthropic" }, { id: "openai-codex" }], getOAuthApiKey: vi.fn(async (provider: string, credentials: Record) => { const credential = credentials[provider]; diff --git a/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts b/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts index a1c636b2685..3361de8ff9e 100644 --- a/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts +++ b/src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts @@ -14,7 +14,7 @@ import type { AuthProfileStore, OAuthCredential } from "./types.js"; let resolveApiKeyForProfile: typeof import("./oauth.js").resolveApiKeyForProfile; let resolveApiKeyForProvider: typeof import("../model-auth.js").resolveApiKeyForProvider; let markAuthProfileSuccess: typeof import("./profiles.js").markAuthProfileSuccess; -type GetOAuthApiKey = typeof import("@earendil-works/pi-ai/oauth").getOAuthApiKey; +type GetOAuthApiKey = typeof import("openclaw/plugin-sdk/llm-oauth").getOAuthApiKey; const { getOAuthApiKeyMock } = vi.hoisted(() => ({ getOAuthApiKeyMock: vi.fn(async () => { @@ -47,7 +47,7 @@ vi.mock("../cli-credentials.js", () => ({ resetCliCredentialCachesForTest: () => undefined, })); -vi.mock("@earendil-works/pi-ai/oauth", () => ({ +vi.mock("openclaw/plugin-sdk/llm-oauth", () => ({ getOAuthApiKey: getOAuthApiKeyMock, getOAuthProviders: () => [ { id: "openai-codex", envApiKey: "OPENAI_API_KEY", oauthTokenEnv: "OPENAI_OAUTH_TOKEN" }, // pragma: allowlist secret @@ -69,7 +69,7 @@ vi.mock("../../plugins/provider-runtime.js", () => ({ })); afterAll(() => { - vi.doUnmock("@earendil-works/pi-ai/oauth"); + vi.doUnmock("openclaw/plugin-sdk/llm-oauth"); vi.doUnmock("../cli-credentials.js"); vi.doUnmock("../../plugins/provider-runtime.runtime.js"); vi.doUnmock("../../plugins/provider-runtime.js"); @@ -163,7 +163,6 @@ describe("resolveApiKeyForProfile openai-codex refresh fallback", () => { await fs.mkdir(agentDir, { recursive: true }); process.env.OPENCLAW_STATE_DIR = caseRoot; process.env.OPENCLAW_AGENT_DIR = agentDir; - process.env.PI_CODING_AGENT_DIR = agentDir; }); afterEach(async () => { diff --git a/src/agents/auth-profiles/oauth.ts b/src/agents/auth-profiles/oauth.ts index 6a9fb1a569a..67f9117d857 100644 --- a/src/agents/auth-profiles/oauth.ts +++ b/src/agents/auth-profiles/oauth.ts @@ -3,7 +3,7 @@ import { getOAuthProviders, type OAuthCredentials, type OAuthProvider, -} from "@earendil-works/pi-ai/oauth"; +} from "openclaw/plugin-sdk/llm-oauth"; import { getRuntimeConfig } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { coerceSecretRef } from "../../config/types.secrets.js"; diff --git a/src/agents/auth-profiles/order.ts b/src/agents/auth-profiles/order.ts index ddced1f5982..3fc2f919abd 100644 --- a/src/agents/auth-profiles/order.ts +++ b/src/agents/auth-profiles/order.ts @@ -130,10 +130,7 @@ function resolveProviderAuthMode( function providerAllowsAwsSdkAuth(cfg: OpenClawConfig | undefined, provider: string): boolean { const authMode = resolveProviderAuthMode(cfg, provider); - return ( - authMode === "aws-sdk" || - (authMode === undefined && normalizeProviderId(provider) === "amazon-bedrock") - ); + return authMode === "aws-sdk"; } export function isConfiguredAwsSdkAuthProfileForProvider(params: { diff --git a/src/agents/bash-process-registry.ts b/src/agents/bash-process-registry.ts index 798ad66944f..2d8fbe39da0 100644 --- a/src/agents/bash-process-registry.ts +++ b/src/agents/bash-process-registry.ts @@ -16,7 +16,7 @@ function clampTtl(value: number | undefined) { return Math.min(Math.max(value, MIN_JOB_TTL_MS), MAX_JOB_TTL_MS); } -let jobTtlMs = clampTtl(Number.parseInt(process.env.PI_BASH_JOB_TTL_MS ?? "", 10)); +let jobTtlMs = clampTtl(Number.parseInt(process.env.OPENCLAW_BASH_JOB_TTL_MS ?? "", 10)); export type ProcessStatus = "running" | "completed" | "failed" | "killed"; diff --git a/src/agents/bash-tools.exec-approval-followup.ts b/src/agents/bash-tools.exec-approval-followup.ts index 18fd2e056e3..1228d7f790a 100644 --- a/src/agents/bash-tools.exec-approval-followup.ts +++ b/src/agents/bash-tools.exec-approval-followup.ts @@ -10,12 +10,12 @@ import { } from "../shared/string-coerce.js"; import { isGatewayMessageChannel, normalizeMessageChannel } from "../utils/message-channel.js"; import { buildExecApprovalFollowupIdempotencyKey } from "./bash-tools.exec-approval-followup-state.js"; +import { sanitizeUserFacingText } from "./embedded-agent-helpers/sanitize-user-facing-text.js"; import { formatExecDeniedUserMessage, isExecDeniedResultText, parseExecApprovalResultText, } from "./exec-approval-result.js"; -import { sanitizeUserFacingText } from "./pi-embedded-helpers/sanitize-user-facing-text.js"; import { callGatewayTool } from "./tools/gateway.js"; type ExecApprovalFollowupParams = { diff --git a/src/agents/bash-tools.exec-gateway-approval.e2e.test.ts b/src/agents/bash-tools.exec-gateway-approval.e2e.test.ts index 5efb87fa40c..cc8b56258ac 100644 --- a/src/agents/bash-tools.exec-gateway-approval.e2e.test.ts +++ b/src/agents/bash-tools.exec-gateway-approval.e2e.test.ts @@ -60,7 +60,7 @@ describe("gateway-hosted exec approvals", () => { clearSessionStoreCacheForTest(); }); - it("lets PI-style gateway tool calls request and wait for approval over separate connections", async () => { + it("lets OpenClaw-style gateway tool calls request and wait for approval over separate connections", async () => { const envSnapshot = captureEnv(TEST_ENV_KEYS); cleanup.push(() => envSnapshot.restore()); diff --git a/src/agents/bash-tools.exec-host-gateway.ts b/src/agents/bash-tools.exec-host-gateway.ts index 194af03a589..ec612103d45 100644 --- a/src/agents/bash-tools.exec-host-gateway.ts +++ b/src/agents/bash-tools.exec-host-gateway.ts @@ -1,4 +1,3 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { describeInterpreterInlineEval } from "../infra/command-analysis/inline-eval.js"; import { detectPolicyInlineEval } from "../infra/command-analysis/policy.js"; import { @@ -50,6 +49,7 @@ import type { ExecApprovalFollowupOutcome, ExecToolDetails, } from "./bash-tools.exec-types.js"; +import type { AgentToolResult } from "./runtime/index.js"; export type ProcessGatewayAllowlistParams = { command: string; diff --git a/src/agents/bash-tools.exec-host-node-phases.ts b/src/agents/bash-tools.exec-host-node-phases.ts index 31119993ce2..fcf59c80826 100644 --- a/src/agents/bash-tools.exec-host-node-phases.ts +++ b/src/agents/bash-tools.exec-host-node-phases.ts @@ -1,5 +1,4 @@ import crypto from "node:crypto"; -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { describeInterpreterInlineEval, type InterpreterInlineEvalHit, @@ -21,6 +20,7 @@ import { normalizeNullableString } from "../shared/string-coerce.js"; import type { ExecuteNodeHostCommandParams } from "./bash-tools.exec-host-node.types.js"; import { renderExecOutputText } from "./bash-tools.exec-output.js"; import type { ExecToolDetails } from "./bash-tools.exec-types.js"; +import type { AgentToolResult } from "./runtime/index.js"; import { callGatewayTool } from "./tools/gateway.js"; import { listNodes, resolveNodeIdFromList } from "./tools/nodes-utils.js"; diff --git a/src/agents/bash-tools.exec-host-node.ts b/src/agents/bash-tools.exec-host-node.ts index d61ca66500d..798c3175002 100644 --- a/src/agents/bash-tools.exec-host-node.ts +++ b/src/agents/bash-tools.exec-host-node.ts @@ -1,4 +1,3 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { APPROVALS_SCOPE, WRITE_SCOPE } from "../gateway/operator-scopes.js"; import { requiresExecApproval, @@ -26,6 +25,7 @@ import { normalizeNotifyOutput, } from "./bash-tools.exec-runtime.js"; import type { ExecToolDetails } from "./bash-tools.exec-types.js"; +import type { AgentToolResult } from "./runtime/index.js"; import { callGatewayTool } from "./tools/gateway.js"; export type { ExecuteNodeHostCommandParams } from "./bash-tools.exec-host-node.types.js"; diff --git a/src/agents/bash-tools.exec-host-shared.ts b/src/agents/bash-tools.exec-host-shared.ts index 551c7b1f5c0..8633822f0a6 100644 --- a/src/agents/bash-tools.exec-host-shared.ts +++ b/src/agents/bash-tools.exec-host-shared.ts @@ -1,5 +1,4 @@ import crypto from "node:crypto"; -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { formatErrorMessage } from "../infra/errors.js"; import { buildExecApprovalUnavailableReplyPayload } from "../infra/exec-approval-reply.js"; import { @@ -26,6 +25,7 @@ import { buildApprovalPendingMessage } from "./bash-tools.exec-runtime.js"; import { DEFAULT_APPROVAL_TIMEOUT_MS } from "./bash-tools.exec-runtime.js"; import type { ExecElevatedDefaults, ExecToolDetails } from "./bash-tools.exec-types.js"; import { isExecDeniedResultText } from "./exec-approval-result.js"; +import type { AgentToolResult } from "./runtime/index.js"; type ResolvedExecApprovals = ReturnType; export const MAX_EXEC_APPROVAL_FOLLOWUP_FAILURE_LOG_KEYS = 256; diff --git a/src/agents/bash-tools.exec-runtime.ts b/src/agents/bash-tools.exec-runtime.ts index 2aa01fd2a81..ecee8ac199a 100644 --- a/src/agents/bash-tools.exec-runtime.ts +++ b/src/agents/bash-tools.exec-runtime.ts @@ -1,5 +1,4 @@ import path from "node:path"; -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { emitDiagnosticEvent } from "../infra/diagnostic-events.js"; import { type EventSessionRoutingPolicy, @@ -22,6 +21,7 @@ import { normalizeStringEntries } from "../shared/string-normalization.js"; import type { ProcessSession } from "./bash-process-registry.js"; import type { ExecToolDetails } from "./bash-tools.exec-types.js"; import type { BashSandboxConfig } from "./bash-tools.shared.js"; +import type { AgentToolResult } from "./runtime/index.js"; export { applyPathPrepend, findPathKey, normalizePathPrepend } from "../infra/path-prepend.js"; export { normalizeExecAsk, @@ -115,7 +115,7 @@ export function validateHostEnv(env: Record): void { } } export const DEFAULT_MAX_OUTPUT = clampWithDefault( - readEnvInt("PI_BASH_MAX_OUTPUT_CHARS"), + readEnvInt("OPENCLAW_BASH_MAX_OUTPUT_CHARS"), 200_000, 1_000, 200_000, @@ -715,7 +715,7 @@ export async function runExecProcess(opts: { return; } const tailText = session.tail || session.aggregated; - // Note: opts.onUpdate() is provided by pi-agent-core's agent-loop and + // Note: opts.onUpdate() is provided by agent runtime's agent-loop and // internally pushes Promise.resolve(emit(event)) into an updateEvents // array. Because emit → processEvents is async, any failure (e.g. // activeRun cleared) produces a *rejected Promise*, not a synchronous diff --git a/src/agents/bash-tools.exec-types.ts b/src/agents/bash-tools.exec-types.ts index dc24a0acc4d..2d1eca235c0 100644 --- a/src/agents/bash-tools.exec-types.ts +++ b/src/agents/bash-tools.exec-types.ts @@ -3,7 +3,7 @@ import type { ExecApprovalDecision } from "../infra/exec-approvals.js"; import type { ExecAsk, ExecHost, ExecSecurity, ExecTarget } from "../infra/exec-approvals.js"; import type { SafeBinProfileFixture } from "../infra/exec-safe-bin-policy.js"; import type { BashSandboxConfig } from "./bash-tools.shared.js"; -import type { EmbeddedFullAccessBlockedReason } from "./pi-embedded-runner/types.js"; +import type { EmbeddedFullAccessBlockedReason } from "./embedded-agent-runner/types.js"; export type ExecToolDefaults = { hasCronTool?: boolean; diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index 829290b574b..a63894c5f2a 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -1,7 +1,6 @@ import { constants as fsConstants } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { buildCommandPayloadCandidates } from "../infra/command-analysis/risks.js"; import { analyzeShellCommand } from "../infra/exec-approvals-analysis.js"; import { @@ -58,6 +57,7 @@ import { resolveWorkdir, truncateMiddle, } from "./bash-tools.shared.js"; +import type { AgentToolResult } from "./runtime/index.js"; import { EXEC_TOOL_DISPLAY_SUMMARY } from "./tool-description-presets.js"; import { type AgentToolWithMeta, failedTextResult, textResult } from "./tools/common.js"; @@ -1219,7 +1219,7 @@ export function createExecTool( defaults?: ExecToolDefaults, ): AgentToolWithMeta { const defaultBackgroundMs = clampWithDefault( - defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"), + defaults?.backgroundMs ?? readEnvInt("OPENCLAW_BASH_YIELD_MS"), 10_000, 10, 120_000, @@ -1641,7 +1641,7 @@ export function createExecTool( const onAbortSignal = () => { // Immediately suppress onUpdate calls so that any late stdout/stderr // from the still-running process cannot push a rejected Promise into - // pi-agent-core's updateEvents after the agent run has ended (#62520). + // agent runtime's updateEvents after the agent run has ended (#62520). // Intentionally placed *before* the yielded/backgrounded guard: the // agent run is ending regardless, so no consumer exists for further // tool_execution_update events even for backgrounded sessions (which diff --git a/src/agents/bash-tools.process-send-keys.ts b/src/agents/bash-tools.process-send-keys.ts index 5c79234da28..f0b30cbe8b3 100644 --- a/src/agents/bash-tools.process-send-keys.ts +++ b/src/agents/bash-tools.process-send-keys.ts @@ -1,7 +1,7 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import type { ProcessSession } from "./bash-process-registry.js"; import { deriveSessionName } from "./bash-tools.shared.js"; import { encodeKeySequence, hasCursorModeSensitiveKeys } from "./pty-keys.js"; +import type { AgentToolResult } from "./runtime/index.js"; export type WritableStdin = { write: (data: string, cb?: (err?: Error | null) => void) => void; diff --git a/src/agents/bash-tools.process.ts b/src/agents/bash-tools.process.ts index e0942d9084f..6c4206a2ab4 100644 --- a/src/agents/bash-tools.process.ts +++ b/src/agents/bash-tools.process.ts @@ -1,4 +1,3 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { formatDurationCompact } from "../infra/format-time/format-duration.ts"; import { getDiagnosticSessionState } from "../logging/diagnostic-session-state.js"; import { killProcessTree } from "../process/kill-tree.js"; @@ -27,6 +26,7 @@ import { } from "./bash-tools.shared.js"; import { recordCommandPoll, resetCommandPollCount } from "./command-poll-backoff.js"; import { encodePaste } from "./pty-keys.js"; +import type { AgentToolResult } from "./runtime/index.js"; import { PROCESS_TOOL_DISPLAY_SUMMARY } from "./tool-description-presets.js"; import type { AgentToolWithMeta } from "./tools/common.js"; diff --git a/src/agents/bootstrap-budget.ts b/src/agents/bootstrap-budget.ts index 39c29adce1b..3f8c8c907d1 100644 --- a/src/agents/bootstrap-budget.ts +++ b/src/agents/bootstrap-budget.ts @@ -1,10 +1,6 @@ import path from "node:path"; import { normalizeOptionalString } from "../shared/string-coerce.js"; -import { - normalizeStringEntries, - normalizeUniqueStringEntries, -} from "../shared/string-normalization.js"; -import type { EmbeddedContextFile } from "./pi-embedded-helpers.js"; +import type { EmbeddedContextFile } from "./embedded-agent-helpers.js"; import type { WorkspaceBootstrapFile } from "./workspace.js"; const DEFAULT_BOOTSTRAP_NEAR_LIMIT_RATIO = 0.85; @@ -77,7 +73,20 @@ function isAgentsBootstrapName(name: string | undefined): boolean { } function normalizeSeenSignatures(signatures?: string[]): string[] { - return normalizeUniqueStringEntries(signatures); + if (!Array.isArray(signatures) || signatures.length === 0) { + return []; + } + const seen = new Set(); + const result: string[] = []; + for (const signature of signatures) { + const value = normalizeOptionalString(signature) ?? ""; + if (!value || seen.has(value)) { + continue; + } + seen.add(value); + result.push(value); + } + return result; } function appendSeenSignature(signatures: string[], signature: string): string[] { @@ -336,7 +345,7 @@ export function appendBootstrapPromptWarning( preserveExactPrompt?: string; }, ): string { - const normalizedLines = normalizeStringEntries(warningLines); + const normalizedLines = (warningLines ?? []).map((line) => line.trim()).filter(Boolean); if (normalizedLines.length === 0) { return prompt; } diff --git a/src/agents/bootstrap-files.ts b/src/agents/bootstrap-files.ts index cebb17002f4..d273e522ce3 100644 --- a/src/agents/bootstrap-files.ts +++ b/src/agents/bootstrap-files.ts @@ -7,13 +7,13 @@ import { resolveUserPath } from "../utils.js"; import { resolveAgentConfig, resolveSessionAgentIds } from "./agent-scope.js"; import { getOrLoadBootstrapFiles } from "./bootstrap-cache.js"; import { applyBootstrapHookOverrides } from "./bootstrap-hooks.js"; -import { shouldIncludeHeartbeatGuidanceForSystemPrompt } from "./heartbeat-system-prompt.js"; -import type { EmbeddedContextFile } from "./pi-embedded-helpers.js"; +import type { EmbeddedContextFile } from "./embedded-agent-helpers.js"; import { buildBootstrapContextFiles, resolveBootstrapMaxChars, resolveBootstrapTotalMaxChars, -} from "./pi-embedded-helpers.js"; +} from "./embedded-agent-helpers.js"; +import { shouldIncludeHeartbeatGuidanceForSystemPrompt } from "./heartbeat-system-prompt.js"; import { DEFAULT_HEARTBEAT_FILENAME, DEFAULT_BOOTSTRAP_FILENAME, diff --git a/src/agents/btw-transcript.ts b/src/agents/btw-transcript.ts index 4c96f424a06..d3459baec60 100644 --- a/src/agents/btw-transcript.ts +++ b/src/agents/btw-transcript.ts @@ -1,16 +1,16 @@ import { readFile } from "node:fs/promises"; -import { - buildSessionContext, - migrateSessionEntries, - parseSessionEntries, - type SessionEntry as PiSessionEntry, -} from "@earendil-works/pi-coding-agent"; import { resolveSessionFilePath, resolveSessionFilePathOptions, type SessionEntry as StoredSessionEntry, } from "../config/sessions.js"; import { diagnosticLogger as diag } from "../logging/diagnostic.js"; +import { + buildSessionContext, + migrateSessionEntries, + parseSessionEntries, + type SessionEntry as AgentSessionEntry, +} from "./sessions/index.js"; export function resolveBtwSessionTranscriptPath(params: { sessionId: string; @@ -33,12 +33,12 @@ export function resolveBtwSessionTranscriptPath(params: { } } -function readSessionEntryId(entry: PiSessionEntry): string | undefined { +function readSessionEntryId(entry: AgentSessionEntry): string | undefined { const id = (entry as { id?: unknown }).id; return typeof id === "string" && id.trim().length > 0 ? id : undefined; } -function readSessionEntryParentId(entry: PiSessionEntry): string | null | undefined { +function readSessionEntryParentId(entry: AgentSessionEntry): string | null | undefined { const parentId = (entry as { parentId?: unknown }).parentId; if (parentId === null) { return null; @@ -46,25 +46,25 @@ function readSessionEntryParentId(entry: PiSessionEntry): string | null | undefi return typeof parentId === "string" && parentId.trim().length > 0 ? parentId : undefined; } -function hasParentLinkedEntries(entries: PiSessionEntry[]): boolean { +function hasParentLinkedEntries(entries: AgentSessionEntry[]): boolean { return entries.some((entry) => Boolean(readSessionEntryId(entry) && "parentId" in entry)); } function buildSessionBranchEntries( - entries: PiSessionEntry[], + entries: AgentSessionEntry[], leafId: string | undefined, -): PiSessionEntry[] | undefined { +): AgentSessionEntry[] | undefined { if (!leafId) { return undefined; } - const byId = new Map(); + const byId = new Map(); for (const entry of entries) { const id = readSessionEntryId(entry); if (id) { byId.set(id, entry); } } - const branch: PiSessionEntry[] = []; + const branch: AgentSessionEntry[] = []; const seen = new Set(); let currentId: string | undefined = leafId; while (currentId) { @@ -82,7 +82,7 @@ function buildSessionBranchEntries( return branch.toReversed(); } -function readDefaultLeafId(entries: PiSessionEntry[]): string | undefined { +function readDefaultLeafId(entries: AgentSessionEntry[]): string | undefined { for (let index = entries.length - 1; index >= 0; index -= 1) { const id = readSessionEntryId(entries[index]); if (id) { @@ -92,7 +92,7 @@ function readDefaultLeafId(entries: PiSessionEntry[]): string | undefined { return undefined; } -function isTrailingUserMessage(entry: PiSessionEntry | undefined): boolean { +function isTrailingUserMessage(entry: AgentSessionEntry | undefined): boolean { return ( entry?.type === "message" && (entry as { message?: { role?: unknown } }).message?.role === "user" @@ -108,7 +108,7 @@ export async function readBtwTranscriptMessages(params: { const entries = parseSessionEntries(await readFile(params.sessionFile, "utf-8")); migrateSessionEntries(entries); const sessionEntries = entries.filter( - (entry): entry is PiSessionEntry => entry.type !== "session", + (entry): entry is AgentSessionEntry => entry.type !== "session", ); if (!hasParentLinkedEntries(sessionEntries)) { return buildSessionContext(sessionEntries).messages; diff --git a/src/agents/btw.test.ts b/src/agents/btw.test.ts index 9cb59c3535d..7e3cfbbafbb 100644 --- a/src/agents/btw.test.ts +++ b/src/agents/btw.test.ts @@ -9,7 +9,6 @@ const buildSessionContextMock = vi.fn(); const ensureOpenClawModelsJsonMock = vi.fn(); const discoverAuthStorageMock = vi.fn(); const discoverModelsMock = vi.fn(); -const resolveModelAsyncMock = vi.fn(); const resolveModelWithRegistryMock = vi.fn(); const ensureAuthProfileStoreMock = vi.fn(); const ensureAuthProfileStoreWithoutExternalProfilesMock = vi.fn(); @@ -26,9 +25,9 @@ const registerProviderStreamForModelMock = vi.fn(); const resolveEmbeddedAgentStreamFnMock = vi.fn(); const diagDebugMock = vi.fn(); -vi.mock("@earendil-works/pi-ai", async () => { +vi.mock("openclaw/plugin-sdk/llm", async () => { const original = - await vi.importActual("@earendil-works/pi-ai"); + await vi.importActual("openclaw/plugin-sdk/llm"); return { ...original, streamSimple: (...args: unknown[]) => streamSimpleMock(...args), @@ -42,7 +41,7 @@ vi.mock("node:fs/promises", () => ({ readFile: (...args: unknown[]) => readFileMock(...args), })); -vi.mock("@earendil-works/pi-coding-agent", () => ({ +vi.mock("openclaw/plugin-sdk/agent-sessions", () => ({ buildSessionContext: (...args: unknown[]) => buildSessionContextMock(...args), generateSummary: vi.fn(async () => "summary"), migrateSessionEntries: (...args: unknown[]) => migrateSessionEntriesMock(...args), @@ -53,13 +52,12 @@ vi.mock("./models-config.js", () => ({ ensureOpenClawModelsJson: (...args: unknown[]) => ensureOpenClawModelsJsonMock(...args), })); -vi.mock("./pi-model-discovery.js", () => ({ +vi.mock("./agent-model-discovery.js", () => ({ discoverAuthStorage: (...args: unknown[]) => discoverAuthStorageMock(...args), discoverModels: (...args: unknown[]) => discoverModelsMock(...args), })); -vi.mock("./pi-embedded-runner/model.js", () => ({ - resolveModelAsync: (...args: unknown[]) => resolveModelAsyncMock(...args), +vi.mock("./embedded-agent-runner/model.js", () => ({ resolveModelWithRegistry: (...args: unknown[]) => resolveModelWithRegistryMock(...args), })); @@ -71,7 +69,7 @@ vi.mock("./model-auth.js", () => ({ requireApiKey: (...args: unknown[]) => requireApiKeyMock(...args), })); -vi.mock("./pi-embedded-runner/runs.js", () => ({ +vi.mock("./embedded-agent-runner/runs.js", () => ({ getActiveEmbeddedRunSnapshot: (...args: unknown[]) => getActiveEmbeddedRunSnapshotMock(...args), })); diff --git a/src/agents/btw.ts b/src/agents/btw.ts index 48edf62c2a3..47ec79e674b 100644 --- a/src/agents/btw.ts +++ b/src/agents/btw.ts @@ -1,23 +1,27 @@ import { streamSimple, - type Api, type AssistantMessageEvent, type ImageContent, type Message, type Model, type TextContent, -} from "@earendil-works/pi-ai"; +} from "openclaw/plugin-sdk/llm"; import type { GetReplyOptions } from "../auto-reply/get-reply-options.types.js"; import type { ReplyPayload } from "../auto-reply/reply-payload.js"; import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js"; import type { SessionEntry as StoredSessionEntry } from "../config/sessions.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { streamWithPayloadPatch } from "../llm/providers/stream-wrappers/stream-payload-utils.js"; import { prepareProviderRuntimeAuth } from "../plugins/provider-runtime.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { discoverAuthStorage, discoverModels } from "./agent-model-discovery.js"; import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "./agent-scope.js"; import { resolveExternalCliAuthOverlayScopeFromSelection } from "./auth-profiles/external-cli-auth-selection.js"; import { resolveSessionAuthProfileOverride } from "./auth-profiles/session-override.js"; import { readBtwTranscriptMessages, resolveBtwSessionTranscriptPath } from "./btw-transcript.js"; +import { EmbeddedBlockChunker, type BlockReplyChunking } from "./embedded-agent-block-chunker.js"; +import { resolveModelWithRegistry } from "./embedded-agent-runner/model.js"; +import { getActiveEmbeddedRunSnapshot } from "./embedded-agent-runner/runs.js"; import { resolveAvailableAgentHarnessPolicy, selectAgentHarness } from "./harness/selection.js"; import { resolveImageSanitizationLimits, @@ -31,12 +35,6 @@ import { } from "./model-auth.js"; import { ensureOpenClawModelsJson } from "./models-config.js"; import { listOpenAIAuthProfileProvidersForAgentRuntime } from "./openai-codex-routing.js"; -import { EmbeddedBlockChunker, type BlockReplyChunking } from "./pi-embedded-block-chunker.js"; -import { resolveModelWithRegistry } from "./pi-embedded-runner/model.js"; -import { getActiveEmbeddedRunSnapshot } from "./pi-embedded-runner/runs.js"; -import { streamWithPayloadPatch } from "./pi-embedded-runner/stream-payload-utils.js"; -import { resolveEmbeddedAgentStreamFn } from "./pi-embedded-runner/stream-resolution.js"; -import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js"; import { registerProviderStreamForModel } from "./provider-stream.js"; import { stripToolResultDetails } from "./session-transcript-repair.js"; import { sanitizeImageBlocks } from "./tool-images.js"; @@ -246,7 +244,7 @@ async function resolveRuntimeModel(params: { storePath?: string; isNewSession: boolean; }): Promise<{ - model: Model; + model: Model; authProfileId?: string; authProfileIdSource?: "auto" | "user"; }> { diff --git a/src/agents/bundle-mcp-config.ts b/src/agents/bundle-mcp-config.ts index b424b6989e8..0049a856d29 100644 --- a/src/agents/bundle-mcp-config.ts +++ b/src/agents/bundle-mcp-config.ts @@ -24,7 +24,7 @@ const OPENCLAW_TRANSPORT_TO_CLI_BUNDLE_TYPE: Record = { /** * User config stores OpenClaw MCP transport names, while CLI backends such as * Claude Code and Gemini expect a downstream `type` field. Keep this adapter - * out of the generic merge path because embedded Pi still consumes the raw + * out of the generic merge path because embedded OpenClaw still consumes the raw * OpenClaw `transport` shape directly. */ export function toCliBundleMcpServerConfig(server: BundleMcpServerConfig): BundleMcpServerConfig { diff --git a/src/agents/cache-trace.ts b/src/agents/cache-trace.ts index ed1e065aad3..a06fd57a2dd 100644 --- a/src/agents/cache-trace.ts +++ b/src/agents/cache-trace.ts @@ -1,6 +1,5 @@ import crypto from "node:crypto"; import path from "node:path"; -import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core"; import { resolveStateDir } from "../config/paths.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveUserPath } from "../utils.js"; @@ -8,6 +7,7 @@ import { parseBooleanValue } from "../utils/boolean.js"; import { safeJsonStringify } from "../utils/safe-json.js"; import { sanitizeDiagnosticPayload } from "./payload-redaction.js"; import { getQueuedFileWriter, type QueuedFileWriter } from "./queued-file-writer.js"; +import type { AgentMessage, StreamFn } from "./runtime/index.js"; import { stableStringify } from "./stable-stringify.js"; import { buildAgentTraceBase } from "./trace-base.js"; diff --git a/src/agents/chutes-oauth.ts b/src/agents/chutes-oauth.ts index 1959afdb852..d82dc5bb6a7 100644 --- a/src/agents/chutes-oauth.ts +++ b/src/agents/chutes-oauth.ts @@ -1,5 +1,5 @@ import { createHash, randomBytes } from "node:crypto"; -import type { OAuthCredentials } from "@earendil-works/pi-ai"; +import type { OAuthCredentials } from "openclaw/plugin-sdk/llm"; import { normalizeOptionalString } from "../shared/string-coerce.js"; const CHUTES_OAUTH_ISSUER = "https://api.chutes.ai"; diff --git a/src/agents/cli-runner.context-engine.test.ts b/src/agents/cli-runner.context-engine.test.ts index d42c53a3851..28af255128f 100644 --- a/src/agents/cli-runner.context-engine.test.ts +++ b/src/agents/cli-runner.context-engine.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ContextEngine } from "../context-engine/types.js"; import type { PreparedCliRunContext } from "./cli-runner/types.js"; diff --git a/src/agents/cli-runner.helpers.test.ts b/src/agents/cli-runner.helpers.test.ts index 8c988acc3b8..5e109f934e9 100644 --- a/src/agents/cli-runner.helpers.test.ts +++ b/src/agents/cli-runner.helpers.test.ts @@ -1,6 +1,6 @@ import fs from "node:fs/promises"; import path from "node:path"; -import type { ImageContent } from "@earendil-works/pi-ai"; +import type { ImageContent } from "openclaw/plugin-sdk/llm"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createSolidPngBuffer } from "../../test/helpers/image-fixtures.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; @@ -14,7 +14,7 @@ import { writeCliImages, writeCliSystemPromptFile, } from "./cli-runner/helpers.js"; -import * as promptImageUtils from "./pi-embedded-runner/run/images.js"; +import * as promptImageUtils from "./embedded-agent-runner/run/images.js"; import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "./system-prompt-cache-boundary.js"; import * as toolImages from "./tool-images.js"; diff --git a/src/agents/cli-runner.reliability.test.ts b/src/agents/cli-runner.reliability.test.ts index f3ed58c2830..af84981334c 100644 --- a/src/agents/cli-runner.reliability.test.ts +++ b/src/agents/cli-runner.reliability.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent"; +import { CURRENT_SESSION_VERSION } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, describe, expect, it, vi } from "vitest"; import { testing as replyRunTesting, diff --git a/src/agents/cli-runner.test-support.ts b/src/agents/cli-runner.test-support.ts index 565899ad861..ad4c1b1e38f 100644 --- a/src/agents/cli-runner.test-support.ts +++ b/src/agents/cli-runner.test-support.ts @@ -5,7 +5,7 @@ import type { enqueueSystemEvent } from "../infra/system-events.js"; import type { getProcessSupervisor } from "../process/supervisor/index.js"; import { setCliRunnerExecuteTestDeps } from "./cli-runner/execute.js"; import { setCliRunnerPrepareTestDeps } from "./cli-runner/prepare.js"; -import type { EmbeddedContextFile } from "./pi-embedded-helpers.js"; +import type { EmbeddedContextFile } from "./embedded-agent-helpers.js"; import type { WorkspaceBootstrapFile } from "./workspace.js"; type ProcessSupervisor = ReturnType; diff --git a/src/agents/cli-runner.ts b/src/agents/cli-runner.ts index c64aad10675..09bfdd7f6f7 100644 --- a/src/agents/cli-runner.ts +++ b/src/agents/cli-runner.ts @@ -1,5 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; import type { ReplyPayload } from "../auto-reply/reply-payload.js"; import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js"; import { formatErrorMessage } from "../infra/errors.js"; @@ -12,6 +10,8 @@ import { loadCliSessionHistoryMessages, } from "./cli-runner/session-history.js"; import type { PreparedCliRunContext, RunCliAgentParams } from "./cli-runner/types.js"; +import { classifyFailoverReason, isFailoverErrorMessage } from "./embedded-agent-helpers.js"; +import type { EmbeddedAgentRunResult } from "./embedded-agent-runner.js"; import { FailoverError, isFailoverError, resolveFailoverStatus } from "./failover-error.js"; import { bootstrapHarnessContextEngine, @@ -26,8 +26,8 @@ import { runAgentHarnessLlmInputHook, runAgentHarnessLlmOutputHook, } from "./harness/lifecycle-hook-helpers.js"; -import { classifyFailoverReason, isFailoverErrorMessage } from "./pi-embedded-helpers.js"; -import type { EmbeddedPiRunResult } from "./pi-embedded-runner.js"; +import type { AgentMessage } from "./runtime/index.js"; +import { SessionManager } from "./sessions/index.js"; const log = createSubsystemLogger("agents/cli-runner"); @@ -210,7 +210,7 @@ async function finalizeCliContextEngineTurn(params: { } } -export async function runCliAgent(params: RunCliAgentParams): Promise { +export async function runCliAgent(params: RunCliAgentParams): Promise { // Cron gate must fire before prepareCliRunContext — that call allocates // backend resources released only by runPreparedCliAgent's try…finally. params.onExecutionStarted?.(); @@ -278,7 +278,7 @@ export async function runCliAgent(params: RunCliAgentParams): Promise { +): Promise { const { executePreparedCliRun } = await import("./cli-runner/execute.runtime.js"); const { params } = context; const hookRunner = getGlobalHookRunner(); @@ -353,7 +353,7 @@ export async function runPreparedCliAgent( durationMs: Date.now() - context.started, }); - const buildBlockedBeforeAgentRunResult = (message: string): EmbeddedPiRunResult => ({ + const buildBlockedBeforeAgentRunResult = (message: string): EmbeddedAgentRunResult => ({ payloads: [{ text: message, isError: true }], meta: { durationMs: Date.now() - context.started, @@ -498,7 +498,7 @@ export async function runPreparedCliAgent( const buildCliRunResult = (resultParams: { output: Awaited>; effectiveCliSessionId?: string; - }): EmbeddedPiRunResult => { + }): EmbeddedAgentRunResult => { const text = resultParams.output.text?.trim(); const rawText = resultParams.output.rawText?.trim(); const payloads = text ? [{ text }] : undefined; @@ -777,6 +777,6 @@ export function buildRunClaudeCliAgentParams(params: RunClaudeCliAgentParams): R export async function runClaudeCliAgent( params: RunClaudeCliAgentParams, -): Promise { +): Promise { return runCliAgent(buildRunClaudeCliAgentParams(params)); } diff --git a/src/agents/cli-runner/claude-live-session.ts b/src/agents/cli-runner/claude-live-session.ts index a0d6f08f118..f3245bce1f2 100644 --- a/src/agents/cli-runner/claude-live-session.ts +++ b/src/agents/cli-runner/claude-live-session.ts @@ -1,14 +1,6 @@ import crypto from "node:crypto"; import type { ReplyBackendHandle } from "../../auto-reply/reply/reply-run-registry.js"; import type { CliBackendConfig } from "../../config/types.js"; -import { isRecord } from "../../shared/record-coerce.js"; -import { - loadExecApprovals, - maxAsk, - minSecurity, - resolveExecApprovalsFromFile, -} from "../../infra/exec-approvals.js"; -import { resolveSessionAgentIds } from "../agent-scope.js"; import { createCliJsonlStreamingParser, extractCliErrorMessage, @@ -16,9 +8,8 @@ import { type CliOutput, type CliStreamingDelta, } from "../cli-output.js"; -import { resolveExecDefaults } from "../exec-defaults.js"; +import { classifyFailoverReason } from "../embedded-agent-helpers.js"; import { FailoverError, resolveFailoverStatus } from "../failover-error.js"; -import { classifyFailoverReason } from "../pi-embedded-helpers.js"; import { cliBackendLog } from "./log.js"; import type { PreparedCliRunContext } from "./types.js"; @@ -55,21 +46,7 @@ type ClaudeLiveSession = { cleanup: () => Promise; cleanupDone: boolean; closing: boolean; - controlRequestPolicy: ClaudeLiveControlRequestPolicy; }; -type ClaudeLiveControlRequestPolicy = { - allowNativeBash: boolean; - security: "deny" | "allowlist" | "full"; - ask: "off" | "on-miss" | "always"; - effectivePermissionMode?: string; -}; - -// OpenClaw exec policy is authoritative for Claude live native Bash. Keep -// Claude's launch mode aligned with the effective OpenClaw policy instead of -// letting raw Claude permission args silently relax or tighten it. -const CLAUDE_BYPASS_PERMISSION_MODE = "bypassPermissions"; -const CLAUDE_DEFAULT_PERMISSION_MODE = "default"; -const CLAUDE_PERMISSION_MODE_FLAG = "--permission-mode"; type ClaudeLiveRunResult = { output: CliOutput; }; @@ -310,7 +287,6 @@ function buildClaudeLiveFingerprint(params: { env: Object.keys(params.env) .toSorted() .map((key) => [key, params.env[key] ? sha256(params.env[key]) : ""]), - controlRequestPolicy: resolveClaudeLiveControlRequestPolicy(params.context, params.argv), }); } @@ -470,172 +446,8 @@ function parseSessionId(parsed: Record): string | undefined { return sessionId || undefined; } -function extractClaudeEffectivePermissionMode(argv: readonly string[]): string | undefined { - // Scan from the end so the last-set --permission-mode wins, matching how - // CLI arg precedence works in practice and how - // extensions/anthropic/cli-shared.ts:normalizeClaudePermissionArgs treats - // operator-provided overrides. - for (let i = argv.length - 1; i >= 0; i -= 1) { - const arg = argv[i] ?? ""; - if (arg === CLAUDE_PERMISSION_MODE_FLAG) { - const value = argv[i + 1]; - if (typeof value === "string" && value.trim().length > 0 && !value.startsWith("-")) { - return value.trim(); - } - continue; - } - if (arg.startsWith(`${CLAUDE_PERMISSION_MODE_FLAG}=`)) { - const value = arg.slice(`${CLAUDE_PERMISSION_MODE_FLAG}=`.length).trim(); - if (value.length > 0 && !value.startsWith("-")) { - return value; - } - } - } - return undefined; -} - -function resolveClaudeLiveControlRequestPolicy( - context: PreparedCliRunContext, - argv?: readonly string[], -): ClaudeLiveControlRequestPolicy { - const execDefaults = resolveExecDefaults({ - cfg: context.params.config, - sessionEntry: context.params.sessionEntry, - agentId: context.params.agentId, - sessionKey: context.params.sessionKey, - }); - const effectiveAgentId = resolveSessionAgentIds({ - sessionKey: context.params.sessionKey, - config: context.params.config, - agentId: context.params.agentId, - }).sessionAgentId; - const approvals = resolveExecApprovalsFromFile({ - file: loadExecApprovals(), - agentId: effectiveAgentId, - overrides: { - security: execDefaults.security, - ask: execDefaults.ask, - }, - }); - const security: ClaudeLiveControlRequestPolicy["security"] = minSecurity( - execDefaults.security, - approvals.agent.security, - ); - const ask: ClaudeLiveControlRequestPolicy["ask"] = maxAsk(execDefaults.ask, approvals.agent.ask); - // Effective permission mode starts from argv so we can detect when the live - // launch needs to be normalized back to OpenClaw's effective exec policy. - const argvMode = argv ? extractClaudeEffectivePermissionMode(argv) : undefined; - const synthesizedMode = - security === "full" && ask === "off" ? CLAUDE_BYPASS_PERMISSION_MODE : undefined; - const effectivePermissionMode = argvMode ?? synthesizedMode; - const allowNativeBash = security === "full" && ask === "off"; - return { - allowNativeBash, - security, - ask, - ...(effectivePermissionMode ? { effectivePermissionMode } : {}), - }; -} - -function shouldForceClaudeLivePermissionPrompt(policy: ClaudeLiveControlRequestPolicy): boolean { - return ( - (policy.security !== "full" || policy.ask !== "off") && - policy.effectivePermissionMode !== CLAUDE_DEFAULT_PERMISSION_MODE - ); -} - -function shouldForceClaudeLiveBypass(policy: ClaudeLiveControlRequestPolicy): boolean { - return ( - policy.security === "full" && - policy.ask === "off" && - policy.effectivePermissionMode !== CLAUDE_BYPASS_PERMISSION_MODE - ); -} - -function writeClaudeLiveControlResponse( - session: ClaudeLiveSession, - response: Record, -): void { - const stdin = session.managedRun.stdin; - if (!stdin) { - closeLiveSession( - session, - "abort", - new Error("Claude CLI live session stdin is unavailable for control response"), - ); - return; - } - stdin.write(`${JSON.stringify(response)}\n`, (error) => { - if (error) { - closeLiveSession(session, "abort", error); - } - }); -} - -function buildClaudeLivePermissionResult( - session: ClaudeLiveSession, - request: Record, -): Record { - const toolName = typeof request.tool_name === "string" ? request.tool_name : ""; - const toolUseID = typeof request.tool_use_id === "string" ? request.tool_use_id : undefined; - if (toolName === "Bash" && session.controlRequestPolicy.allowNativeBash) { - return { - behavior: "allow", - updatedInput: isRecord(request.input) ? request.input : {}, - ...(toolUseID ? { toolUseID } : {}), - }; - } - const permissionModeNote = session.controlRequestPolicy.effectivePermissionMode - ? `, permission-mode=${session.controlRequestPolicy.effectivePermissionMode}` - : ""; - const message = - toolName === "Bash" - ? `OpenClaw denied Claude native Bash because the effective exec policy is security=${session.controlRequestPolicy.security}, ask=${session.controlRequestPolicy.ask}${permissionModeNote}; this bridge only auto-allows Bash when OpenClaw exec is full/no-ask.` - : `OpenClaw denied Claude native ${toolName || "tool"}; this bridge only maps Claude native Bash permission prompts to OpenClaw exec policy. Use OpenClaw MCP tools instead.`; - return { - behavior: "deny", - message, - ...(toolUseID ? { toolUseID } : {}), - decisionClassification: "user_reject", - }; -} - -function handleClaudeLiveControlRequest( - session: ClaudeLiveSession, - parsed: Record, -): void { - const requestId = typeof parsed.request_id === "string" ? parsed.request_id : ""; - const request = isRecord(parsed.request) ? parsed.request : null; - if (!requestId || !request) { - cliBackendLog.warn("claude live control_request ignored: malformed request"); - return; - } - if (request.subtype === "can_use_tool") { - const permissionResult = buildClaudeLivePermissionResult(session, request); - const toolName = typeof request.tool_name === "string" ? request.tool_name : "unknown"; - cliBackendLog.info( - `claude live control_request: subtype=can_use_tool tool=${toolName} decision=${permissionResult.behavior as string}`, - ); - writeClaudeLiveControlResponse(session, { - type: "control_response", - response: { - subtype: "success", - request_id: requestId, - response: permissionResult, - }, - }); - return; - } - const subtype = typeof request.subtype === "string" ? request.subtype : "unknown"; - cliBackendLog.warn(`claude live control_request denied: unsupported subtype=${subtype}`); - writeClaudeLiveControlResponse(session, { - type: "control_response", - response: { - subtype: "error", - request_id: requestId, - error: `OpenClaw Claude live bridge does not support control request subtype '${subtype}'.`, - }, - }); +function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === "object" && !Array.isArray(value)); } function normalizePositiveInt( @@ -719,13 +531,6 @@ function handleClaudeLiveLine(session: ClaudeLiveSession, line: string): void { if (!parsed) { return; } - if (parsed.type === "control_request") { - handleClaudeLiveControlRequest(session, parsed); - return; - } - if (parsed.type === "control_response") { - return; - } if (session.drainingAbortedTurn) { if (parsed.type === "result") { const turnToClear = session.currentTurn; @@ -935,7 +740,6 @@ async function createClaudeLiveSession(params: { cleanup: params.cleanup, cleanupDone: false, closing: false, - controlRequestPolicy: resolveClaudeLiveControlRequestPolicy(params.context, params.argv), }; void managedRun.wait().then( (exit) => handleClaudeExit(session, exit.exitCode), @@ -1041,25 +845,15 @@ export async function runClaudeLiveSessionTurn(params: { }): Promise { const key = buildClaudeLiveKey(params.context); const resumeCapable = Boolean(params.context.preparedBackend.backend.resumeArgs?.length); - let liveArgs = buildClaudeLiveArgs({ - args: params.args, - backend: params.context.preparedBackend.backend, - systemPrompt: params.context.systemPrompt, - useResume: params.useResume, - }); - let argv = [params.context.preparedBackend.backend.command, ...liveArgs]; - const launchPolicy = resolveClaudeLiveControlRequestPolicy(params.context, argv); - if (shouldForceClaudeLivePermissionPrompt(launchPolicy)) { - liveArgs = upsertArgValue( - liveArgs, - CLAUDE_PERMISSION_MODE_FLAG, - CLAUDE_DEFAULT_PERMISSION_MODE, - ); - argv = [params.context.preparedBackend.backend.command, ...liveArgs]; - } else if (shouldForceClaudeLiveBypass(launchPolicy)) { - liveArgs = upsertArgValue(liveArgs, CLAUDE_PERMISSION_MODE_FLAG, CLAUDE_BYPASS_PERMISSION_MODE); - argv = [params.context.preparedBackend.backend.command, ...liveArgs]; - } + const argv = [ + params.context.preparedBackend.backend.command, + ...buildClaudeLiveArgs({ + args: params.args, + backend: params.context.preparedBackend.backend, + systemPrompt: params.context.systemPrompt, + useResume: params.useResume, + }), + ]; const fingerprint = buildClaudeLiveFingerprint({ context: params.context, argv, diff --git a/src/agents/cli-runner/execute.ts b/src/agents/cli-runner/execute.ts index 8ffeeceac5d..1ccac83dd95 100644 --- a/src/agents/cli-runner/execute.ts +++ b/src/agents/cli-runner/execute.ts @@ -18,8 +18,8 @@ import { parseCliOutput, type CliOutput, } from "../cli-output.js"; +import { classifyFailoverReason } from "../embedded-agent-helpers.js"; import { FailoverError, resolveFailoverStatus } from "../failover-error.js"; -import { classifyFailoverReason } from "../pi-embedded-helpers.js"; import { applyPluginTextReplacements } from "../plugin-text-transforms.js"; import { applySkillEnvOverridesFromSnapshot } from "../skills.js"; import { runClaudeLiveSessionTurn, shouldUseClaudeLiveSession } from "./claude-live-session.js"; diff --git a/src/agents/cli-runner/helpers.system-prompt.test.ts b/src/agents/cli-runner/helpers.system-prompt.test.ts index b7429537847..422775355df 100644 --- a/src/agents/cli-runner/helpers.system-prompt.test.ts +++ b/src/agents/cli-runner/helpers.system-prompt.test.ts @@ -35,14 +35,14 @@ describe("buildCliAgentSystemPrompt", () => { expect(prompt).not.toContain("Do not poll `subagents list` / `sessions_list` in a loop"); }); - it("uses CLI backend tool fallback instead of PI tool assumptions", () => { + it("uses CLI backend tool fallback instead of OpenClaw tool assumptions", () => { const prompt = buildCliAgentSystemPrompt({ workspaceDir: "/tmp/openclaw", tools: [], modelDisplay: "test/model", }); - expect(prompt).not.toContain("Pi lists the standard tools above"); + expect(prompt).not.toContain("OpenClaw lists the standard tools above"); expect(prompt).not.toContain("This runtime enables:"); expect(prompt).not.toContain("For long waits, avoid rapid poll loops"); expect(prompt).not.toContain("Larger work: use `sessions_spawn`"); @@ -60,8 +60,8 @@ describe("buildCliAgentSystemPrompt", () => { surfaces: ["cli_backend"], }, { - text: "PI-only command guidance.", - surfaces: ["pi_main"], + text: "OpenClaw-only command guidance.", + surfaces: ["openclaw_main"], }, ], handler: async () => ({ text: "ok" }), @@ -74,6 +74,6 @@ describe("buildCliAgentSystemPrompt", () => { }); expect(prompt).toContain("CLI-only command guidance."); - expect(prompt).not.toContain("PI-only command guidance."); + expect(prompt).not.toContain("OpenClaw-only command guidance."); }); }); diff --git a/src/agents/cli-runner/helpers.ts b/src/agents/cli-runner/helpers.ts index 062646766c5..f1547d855b2 100644 --- a/src/agents/cli-runner/helpers.ts +++ b/src/agents/cli-runner/helpers.ts @@ -2,9 +2,8 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AgentTool } from "@earendil-works/pi-agent-core"; -import type { ImageContent } from "@earendil-works/pi-ai"; import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue"; +import type { ImageContent } from "openclaw/plugin-sdk/llm"; import { isAcpRuntimeSpawnAvailable } from "../../acp/runtime/availability.js"; import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js"; import type { ThinkLevel } from "../../auto-reply/thinking.js"; @@ -20,9 +19,10 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, } from "../../shared/string-coerce.js"; +import type { EmbeddedContextFile } from "../embedded-agent-helpers.js"; +import { detectImageReferences, loadImageFromRef } from "../embedded-agent-runner/run/images.js"; import { resolveDefaultModelForAgent } from "../model-selection.js"; -import type { EmbeddedContextFile } from "../pi-embedded-helpers.js"; -import { detectImageReferences, loadImageFromRef } from "../pi-embedded-runner/run/images.js"; +import type { AgentTool } from "../runtime/index.js"; import type { SandboxFsBridge } from "../sandbox/fs-bridge.js"; import { detectRuntimeShell } from "../shell-utils.js"; import { stripSystemPromptCacheBoundary } from "../system-prompt-cache-boundary.js"; diff --git a/src/agents/cli-runner/prepare.test.ts b/src/agents/cli-runner/prepare.test.ts index faeed1ed6b8..d964e4bbe19 100644 --- a/src/agents/cli-runner/prepare.test.ts +++ b/src/agents/cli-runner/prepare.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent"; +import { CURRENT_SESSION_VERSION } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { registerLegacyContextEngine } from "../../context-engine/legacy.registration.js"; @@ -717,7 +717,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => { hostRequirements: { "agent-run": { requiredCapabilities: ["assemble-before-prompt"], - unsupportedMessage: "Use the native Codex or Pi embedded runtime.", + unsupportedMessage: "Use the native Codex or OpenClaw embedded runtime.", }, }, }, diff --git a/src/agents/cli-runner/prepare.ts b/src/agents/cli-runner/prepare.ts index 7b6eecdb980..cbad4341ece 100644 --- a/src/agents/cli-runner/prepare.ts +++ b/src/agents/cli-runner/prepare.ts @@ -42,16 +42,16 @@ import { claudeCliSessionTranscriptHasContent } from "../command/attempt-executi import { resolveContextWindowInfo } from "../context-window-guard.js"; import { resolveContextTokensForModel } from "../context.js"; import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js"; -import { resolveHeartbeatPromptForSystemPrompt } from "../heartbeat-system-prompt.js"; import { resolveBootstrapMaxChars, resolveBootstrapPromptTruncationWarningMode, resolveBootstrapTotalMaxChars, -} from "../pi-embedded-helpers.js"; -import { resolvePromptBuildHookResult } from "../pi-embedded-runner/run/attempt.prompt-helpers.js"; -import { resolveAttemptPrependSystemContext } from "../pi-embedded-runner/run/attempt.prompt-helpers.js"; -import { composeSystemPromptWithHookContext } from "../pi-embedded-runner/run/attempt.thread-helpers.js"; -import { buildCurrentInboundPrompt } from "../pi-embedded-runner/run/runtime-context-prompt.js"; +} from "../embedded-agent-helpers.js"; +import { resolvePromptBuildHookResult } from "../embedded-agent-runner/run/attempt.prompt-helpers.js"; +import { resolveAttemptPrependSystemContext } from "../embedded-agent-runner/run/attempt.prompt-helpers.js"; +import { composeSystemPromptWithHookContext } from "../embedded-agent-runner/run/attempt.thread-helpers.js"; +import { buildCurrentInboundPrompt } from "../embedded-agent-runner/run/runtime-context-prompt.js"; +import { resolveHeartbeatPromptForSystemPrompt } from "../heartbeat-system-prompt.js"; import { applyPluginTextReplacements } from "../plugin-text-transforms.js"; import { resolveSkillsPromptForRun } from "../skills.js"; import { resolveSystemPromptOverride } from "../system-prompt-override.js"; diff --git a/src/agents/cli-runner/reliability.ts b/src/agents/cli-runner/reliability.ts index 8c0b8ce8446..ff9c373f157 100644 --- a/src/agents/cli-runner/reliability.ts +++ b/src/agents/cli-runner/reliability.ts @@ -6,7 +6,7 @@ import { CLI_RESUME_WATCHDOG_DEFAULTS, CLI_WATCHDOG_MIN_TIMEOUT_MS, } from "../cli-watchdog-defaults.js"; -import type { EmbeddedRunTrigger } from "../pi-embedded-runner/run/params.js"; +import type { EmbeddedRunTrigger } from "../embedded-agent-runner/run/params.js"; function pickWatchdogProfile( backend: CliBackendConfig, diff --git a/src/agents/cli-runner/session-history.test.ts b/src/agents/cli-runner/session-history.test.ts index e5321f8df03..7d152c7d558 100644 --- a/src/agents/cli-runner/session-history.test.ts +++ b/src/agents/cli-runner/session-history.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent"; +import { CURRENT_SESSION_VERSION } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, describe, expect, it, vi } from "vitest"; import { buildCliSessionHistoryPrompt, diff --git a/src/agents/cli-runner/session-history.ts b/src/agents/cli-runner/session-history.ts index 3eb700173af..d473ce289c4 100644 --- a/src/agents/cli-runner/session-history.ts +++ b/src/agents/cli-runner/session-history.ts @@ -1,7 +1,5 @@ import fsp from "node:fs/promises"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { migrateSessionEntries, parseSessionEntries } from "@earendil-works/pi-coding-agent"; import { resolveSessionFilePath, resolveSessionFilePathOptions, @@ -13,6 +11,8 @@ import { limitAgentHookHistoryMessages, MAX_AGENT_HOOK_HISTORY_MESSAGES, } from "../harness/hook-history.js"; +import type { AgentMessage } from "../runtime/index.js"; +import { migrateSessionEntries, parseSessionEntries } from "../sessions/index.js"; export const MAX_CLI_SESSION_HISTORY_FILE_BYTES = 5 * 1024 * 1024; export const MAX_CLI_SESSION_HISTORY_MESSAGES = MAX_AGENT_HOOK_HISTORY_MESSAGES; diff --git a/src/agents/cli-runner/types.ts b/src/agents/cli-runner/types.ts index b54d5f2e586..da31bdc7f6f 100644 --- a/src/agents/cli-runner/types.ts +++ b/src/agents/cli-runner/types.ts @@ -1,4 +1,4 @@ -import type { ImageContent } from "@earendil-works/pi-ai"; +import type { ImageContent } from "openclaw/plugin-sdk/llm"; import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js"; import type { ReplyOperation } from "../../auto-reply/reply/reply-run-registry.js"; import type { ThinkLevel } from "../../auto-reply/thinking.js"; @@ -17,11 +17,11 @@ import type { import type { BootstrapContextMode } from "../bootstrap-files.js"; import type { ResolvedCliBackend } from "../cli-backends.js"; import type { ContextWindowInfo } from "../context-window-guard.js"; -import type { EmbeddedAgentExecutionPhase } from "../pi-embedded-runner/execution-phase.js"; +import type { EmbeddedAgentExecutionPhase } from "../embedded-agent-runner/execution-phase.js"; import type { CurrentInboundPromptContext, EmbeddedRunTrigger, -} from "../pi-embedded-runner/run/params.js"; +} from "../embedded-agent-runner/run/params.js"; import type { SkillSnapshot } from "../skills.js"; import type { SilentReplyPromptMode } from "../system-prompt.types.js"; diff --git a/src/agents/code-mode.ts b/src/agents/code-mode.ts index efff921c846..a990c496883 100644 --- a/src/agents/code-mode.ts +++ b/src/agents/code-mode.ts @@ -2,21 +2,21 @@ import { randomUUID } from "node:crypto"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { Worker } from "node:worker_threads"; -import type { AgentToolUpdateCallback } from "@earendil-works/pi-agent-core"; -import type { ToolDefinition } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { isRecord } from "../shared/record-coerce.js"; import { uniqueValues } from "../shared/string-normalization.js"; import { resolveAgentConfig } from "./agent-scope-config.js"; +import type { HookContext } from "./agent-tools.before-tool-call.js"; import { CODE_MODE_EXEC_TOOL_NAME, CODE_MODE_WAIT_TOOL_NAME, isCodeModeControlTool, markCodeModeControlTool, } from "./code-mode-control-tools.js"; -import type { HookContext } from "./pi-tools.before-tool-call.js"; +import type { AgentToolUpdateCallback } from "./runtime/index.js"; import { optionalStringEnum } from "./schema/typebox.js"; +import type { ToolDefinition } from "./sessions/index.js"; import { addClientToolsToToolCatalog, applyToolCatalogCompaction, @@ -470,7 +470,7 @@ async function runBridgeRequest(params: { parentToolCallId: string; request: PendingBridgeRequest; signal?: AbortSignal; - onUpdate?: AgentToolUpdateCallback; + onUpdate?: AgentToolUpdateCallback; }): Promise { try { const values = Array.isArray(params.request.args) ? params.request.args : []; @@ -622,7 +622,7 @@ function snapshotState(params: { runtime: ToolSearchRuntime; output: unknown[]; signal?: AbortSignal; - onUpdate?: AgentToolUpdateCallback; + onUpdate?: AgentToolUpdateCallback; }) { enforceActiveRunLimit(); if (params.snapshotBytes.byteLength > params.config.maxSnapshotBytes) { @@ -690,7 +690,7 @@ async function runExec(params: { code: string; language?: CodeModeLanguage; signal?: AbortSignal; - onUpdate?: AgentToolUpdateCallback; + onUpdate?: AgentToolUpdateCallback; }) { removeExpiredRuns(); const config = resolveCodeModeConfig( @@ -781,7 +781,7 @@ async function runWait(params: { ctx: CodeModeToolContext; runId: string; signal?: AbortSignal; - onUpdate?: AgentToolUpdateCallback; + onUpdate?: AgentToolUpdateCallback; }) { removeExpiredRuns(); const state = activeRuns.get(params.runId); @@ -897,7 +897,7 @@ export function createCodeModeTools(ctx: CodeModeToolContext): AnyAgentTool[] { toolCallId: string, args: unknown, signal?: AbortSignal, - onUpdate?: AgentToolUpdateCallback, + onUpdate?: AgentToolUpdateCallback, ) => { const input = readCode(args); return jsonResult( @@ -923,7 +923,7 @@ export function createCodeModeTools(ctx: CodeModeToolContext): AnyAgentTool[] { toolCallId: string, args: unknown, signal?: AbortSignal, - onUpdate?: AgentToolUpdateCallback, + onUpdate?: AgentToolUpdateCallback, ) => jsonResult( await runWait({ diff --git a/src/agents/codex-app-server.extensions.test.ts b/src/agents/codex-app-server.extensions.test.ts index e27c3287297..d3dc2765337 100644 --- a/src/agents/codex-app-server.extensions.test.ts +++ b/src/agents/codex-app-server.extensions.test.ts @@ -85,7 +85,7 @@ describe("agent tool result middleware", () => { loadOpenClawPlugins(options); expect(listAgentToolResultMiddlewares("codex")).toHaveLength(1); - expect(listAgentToolResultMiddlewares("pi")).toHaveLength(0); + expect(listAgentToolResultMiddlewares("openclaw")).toHaveLength(0); resetActivePluginRegistryForTest(); expect(listAgentToolResultMiddlewares("codex")).toHaveLength(0); @@ -114,7 +114,7 @@ describe("agent tool result middleware", () => { filename: "index.mjs", manifest: { contracts: { - agentToolResultMiddleware: ["pi"], + agentToolResultMiddleware: ["openclaw"], }, }, body: `export default { id: "tool-result-middleware", register(api) { @@ -191,12 +191,12 @@ describe("agent tool result middleware", () => { filename: "index.mjs", manifest: { contracts: { - agentToolResultMiddleware: ["pi", "codex"], + agentToolResultMiddleware: ["openclaw", "codex"], }, }, body: `const middleware = () => undefined; export default { id: "tool-result-middleware", register(api) { - api.registerAgentToolResultMiddleware(middleware, { runtimes: ["pi"] }); + api.registerAgentToolResultMiddleware(middleware, { runtimes: ["openclaw"] }); api.registerAgentToolResultMiddleware(middleware, { runtimes: ["codex"] }); } };`, }); @@ -214,7 +214,7 @@ export default { id: "tool-result-middleware", register(api) { }, }); - expect(listAgentToolResultMiddlewares("pi")).toHaveLength(1); + expect(listAgentToolResultMiddlewares("openclaw")).toHaveLength(1); expect(listAgentToolResultMiddlewares("codex")).toHaveLength(1); }); diff --git a/src/agents/codex-mcp-config.test.ts b/src/agents/codex-mcp-config.test.ts index c635bb65e8f..5392ab217d0 100644 --- a/src/agents/codex-mcp-config.test.ts +++ b/src/agents/codex-mcp-config.test.ts @@ -131,7 +131,7 @@ describe("loadCodexBundleMcpThreadConfig", () => { expect(loaded.evaluated).toBe(true); }); - it("returns an evaluated empty MCP config when Pi would not create a bundle MCP runtime", () => { + it("returns an evaluated empty MCP config when no bundle MCP runtime is needed", () => { const cfg = { mcp: { servers: { diff --git a/src/agents/codex-mcp-config.ts b/src/agents/codex-mcp-config.ts index f7db7a0f0ed..5292342b640 100644 --- a/src/agents/codex-mcp-config.ts +++ b/src/agents/codex-mcp-config.ts @@ -16,7 +16,7 @@ import type { CodexMcpServersConfig, LoadCodexBundleMcpThreadConfigParams, } from "./codex-mcp-config.types.js"; -import { shouldCreateBundleMcpRuntimeForAttempt } from "./pi-embedded-runner/run/attempt-tool-construction-plan.js"; +import { shouldCreateBundleMcpRuntimeForAttempt } from "./embedded-agent-runner/run/attempt-tool-construction-plan.js"; export type { CodexBundleMcpThreadConfig, diff --git a/src/agents/command/attempt-callbacks.ts b/src/agents/command/attempt-callbacks.ts index eca7cdbf1de..def48aa6318 100644 --- a/src/agents/command/attempt-callbacks.ts +++ b/src/agents/command/attempt-callbacks.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../runtime/index.js"; export type AgentAttemptLifecycleState = { currentTurnUserMessagePersisted: boolean; diff --git a/src/agents/command/attempt-execution.cli.test.ts b/src/agents/command/attempt-execution.cli.test.ts index 614a15c179f..a3a52cd99a1 100644 --- a/src/agents/command/attempt-execution.cli.test.ts +++ b/src/agents/command/attempt-execution.cli.test.ts @@ -5,12 +5,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { SessionEntry } from "../../config/sessions.js"; import { appendSessionTranscriptMessage } from "../../config/sessions/transcript-append.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { runEmbeddedAgent, type EmbeddedAgentRunResult } from "../embedded-agent.js"; import { FailoverError } from "../failover-error.js"; -import { runEmbeddedPiAgent, type EmbeddedPiRunResult } from "../pi-embedded.js"; import { persistCliTurnTranscript, runAgentAttempt } from "./attempt-execution.js"; const runCliAgentMock = vi.hoisted(() => vi.fn()); -const runEmbeddedPiAgentMock = vi.hoisted(() => vi.fn()); +const runEmbeddedAgentMock = vi.hoisted(() => vi.fn()); const ORIGINAL_HOME = process.env.HOME; vi.mock("../cli-runner.js", () => ({ @@ -29,11 +29,11 @@ vi.mock("../provider-auth-aliases.js", () => ({ provider.trim().toLowerCase() === "codex-cli" ? "openai-codex" : provider.trim().toLowerCase(), })); -vi.mock("../pi-embedded.js", () => ({ - runEmbeddedPiAgent: runEmbeddedPiAgentMock, +vi.mock("../embedded-agent.js", () => ({ + runEmbeddedAgent: runEmbeddedAgentMock, })); -function makeCliResult(text: string): EmbeddedPiRunResult { +function makeCliResult(text: string): EmbeddedAgentRunResult { return { payloads: [{ text }], meta: { @@ -125,8 +125,8 @@ function firstRunCliAgentArg(callIndex = 0) { return requireMockArg(runCliAgentMock, callIndex, "run CLI agent argument"); } -function firstEmbeddedPiAgentArg(callIndex = 0) { - return requireMockArg(runEmbeddedPiAgentMock, callIndex, "embedded PI agent argument"); +function firstEmbeddedAgentArg(callIndex = 0) { + return requireMockArg(runEmbeddedAgentMock, callIndex, "embedded OpenClaw agent argument"); } describe("CLI attempt execution", () => { @@ -137,7 +137,7 @@ describe("CLI attempt execution", () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-attempt-")); storePath = path.join(tmpDir, "sessions.json"); runCliAgentMock.mockReset(); - runEmbeddedPiAgentMock.mockReset(); + runEmbeddedAgentMock.mockReset(); }); afterEach(async () => { @@ -1013,7 +1013,7 @@ describe("CLI attempt execution", () => { sessionHasHistory: false, }); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); expectMockArgFields(runCliAgentMock, { provider: "claude-cli", model: "claude-opus-4-7", @@ -1068,7 +1068,7 @@ describe("CLI attempt execution", () => { sessionHasHistory: false, }); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); expectMockArgFields(runCliAgentMock, { provider: "claude-cli", model: "opus-4.7", @@ -1083,12 +1083,12 @@ describe("CLI attempt execution", () => { }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "canonical codex embedded" }], meta: { durationMs: 5, finalAssistantVisibleText: "canonical codex embedded", - executionTrace: { runner: "pi" }, + executionTrace: { runner: "openclaw" }, }, }); @@ -1131,7 +1131,7 @@ describe("CLI attempt execution", () => { }); expect(runCliAgentMock).not.toHaveBeenCalled(); - expectMockArgFields(runEmbeddedPiAgentMock, { + expectMockArgFields(runEmbeddedAgentMock, { provider: "openai", model: "gpt-5.4", }); @@ -1173,9 +1173,9 @@ describe("CLI attempt execution", () => { supports: () => ({ supported: true, priority: 100 }), runAttempt: vi.fn(), }); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); try { await runAgentAttempt({ @@ -1211,7 +1211,7 @@ describe("CLI attempt execution", () => { clearAgentHarnesses(); } - expectMockArgFields(runEmbeddedPiAgentMock, { + expectMockArgFields(runEmbeddedAgentMock, { provider: "openai", model: "gpt-5.4", authProfileId: "openai:backup", @@ -1227,9 +1227,9 @@ describe("CLI attempt execution", () => { }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "anthropic", @@ -1277,10 +1277,10 @@ describe("CLI attempt execution", () => { }); expect(runCliAgentMock).not.toHaveBeenCalled(); - expectMockArgFields(runEmbeddedPiAgentMock, { + expectMockArgFields(runEmbeddedAgentMock, { provider: "anthropic", model: "claude-opus-4-7", - agentHarnessId: "pi", + agentHarnessId: "openclaw", prompt: "raw prompt", messageChannel: "discord", messageProvider: "discord-voice", @@ -1288,7 +1288,7 @@ describe("CLI attempt execution", () => { promptMode: "none", disableTools: true, }); - expect(firstEmbeddedPiAgentArg().prompt).not.toContain("[Inter-session message]"); + expect(firstEmbeddedAgentArg().prompt).not.toContain("[Inter-session message]"); }); it("forwards trusted elevated defaults to embedded agent runs", async () => { @@ -1304,9 +1304,9 @@ describe("CLI attempt execution", () => { defaultLevel: "on" as const, }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", @@ -1340,7 +1340,7 @@ describe("CLI attempt execution", () => { sessionHasHistory: false, }); - expectMockArgFields(runEmbeddedPiAgentMock, { + expectMockArgFields(runEmbeddedAgentMock, { provider: "openai", model: "gpt-5.4", bashElevated, @@ -1394,7 +1394,7 @@ describe("CLI attempt execution", () => { cleanupBundleMcpOnRunEnd: true, cleanupCliLiveSessionOnRunEnd: true, }); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); }); @@ -1404,7 +1404,7 @@ describe("embedded attempt harness pinning", () => { beforeEach(async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-embedded-attempt-")); runCliAgentMock.mockReset(); - runEmbeddedPiAgentMock.mockReset(); + runEmbeddedAgentMock.mockReset(); }); afterEach(async () => { @@ -1416,9 +1416,9 @@ describe("embedded attempt harness pinning", () => { sessionId: "legacy-session", updatedAt: Date.now(), }; - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", @@ -1435,7 +1435,7 @@ describe("embedded attempt harness pinning", () => { isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, - runId: "run-legacy-pi-pin", + runId: "run-legacy-runtime-pin", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, @@ -1448,7 +1448,7 @@ describe("embedded attempt harness pinning", () => { sessionHasHistory: true, }); - expectMockArgFields(runEmbeddedPiAgentMock, { agentHarnessId: undefined }); + expectMockArgFields(runEmbeddedAgentMock, { agentHarnessId: undefined }); }); it("ignores stale session Codex harness pins on non-OpenAI model switches", async () => { @@ -1457,9 +1457,9 @@ describe("embedded attempt harness pinning", () => { updatedAt: Date.now(), agentHarnessId: "codex", }; - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "minimax", @@ -1489,7 +1489,7 @@ describe("embedded attempt harness pinning", () => { sessionHasHistory: true, }); - expectMockArgFields(runEmbeddedPiAgentMock, { agentHarnessId: undefined }); + expectMockArgFields(runEmbeddedAgentMock, { agentHarnessId: undefined }); }); it("forwards runtime toolsAllow into embedded attempts", async () => { @@ -1497,9 +1497,9 @@ describe("embedded attempt harness pinning", () => { sessionId: "tools-allow-session", updatedAt: Date.now(), }; - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", @@ -1531,7 +1531,7 @@ describe("embedded attempt harness pinning", () => { sessionHasHistory: false, }); - expectMockArgFields(runEmbeddedPiAgentMock, { toolsAllow: ["read", "web_search"] }); + expectMockArgFields(runEmbeddedAgentMock, { toolsAllow: ["read", "web_search"] }); }); it("lets provider/model runtime policy choose Codex without storing a session harness pin", async () => { @@ -1539,9 +1539,9 @@ describe("embedded attempt harness pinning", () => { sessionId: "codex-history-session", updatedAt: Date.now(), }; - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "codex", @@ -1568,7 +1568,7 @@ describe("embedded attempt harness pinning", () => { isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, - runId: "run-codex-no-pi-pin", + runId: "run-codex-no-runtime-pin", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, @@ -1581,7 +1581,7 @@ describe("embedded attempt harness pinning", () => { sessionHasHistory: true, }); - expectMockArgFields(runEmbeddedPiAgentMock, { agentHarnessId: undefined }); + expectMockArgFields(runEmbeddedAgentMock, { agentHarnessId: undefined }); }); it("auto-forwards OpenAI Codex auth profiles to default Codex harness runs", async () => { @@ -1605,9 +1605,9 @@ describe("embedded attempt harness pinning", () => { }, }), ); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); clearAgentHarnesses(); registerAgentHarness({ id: "codex", @@ -1648,7 +1648,7 @@ describe("embedded attempt harness pinning", () => { clearAgentHarnesses(); } - expectMockArgFields(runEmbeddedPiAgentMock, { + expectMockArgFields(runEmbeddedAgentMock, { agentHarnessId: undefined, authProfileId: "openai-codex:work", authProfileIdSource: "auto", @@ -1660,9 +1660,9 @@ describe("embedded attempt harness pinning", () => { sessionId: "fresh-session", updatedAt: Date.now(), }; - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", @@ -1692,18 +1692,18 @@ describe("embedded attempt harness pinning", () => { sessionHasHistory: false, }); - expectMockArgFields(runEmbeddedPiAgentMock, { agentHarnessId: undefined }); + expectMockArgFields(runEmbeddedAgentMock, { agentHarnessId: undefined }); }); - it("ignores stale OpenAI sessions pinned to PI and relies on default Codex routing", async () => { + it("ignores stale OpenAI sessions pinned to OpenClaw and relies on default Codex routing", async () => { const sessionEntry: SessionEntry = { - sessionId: "stale-pi-session", + sessionId: "stale-agent-session", updatedAt: Date.now(), - agentHarnessId: "pi", + agentHarnessId: "openclaw", }; - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", @@ -1720,7 +1720,7 @@ describe("embedded attempt harness pinning", () => { isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, - runId: "run-stale-openai-pi-pin", + runId: "run-stale-openai-runtime-pin", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, @@ -1733,22 +1733,22 @@ describe("embedded attempt harness pinning", () => { sessionHasHistory: true, }); - expectMockArgFields(runEmbeddedPiAgentMock, { + expectMockArgFields(runEmbeddedAgentMock, { provider: "openai", agentHarnessId: undefined, }); }); - it("routes explicit OpenAI PI runs with Codex OAuth through PI and the legacy Codex auth transport", async () => { + it("routes explicit OpenAI native runs with Codex OAuth through OpenClaw and the legacy Codex auth transport", async () => { const sessionEntry: SessionEntry = { - sessionId: "explicit-pi-codex-oauth-session", + sessionId: "explicit-agent-codex-oauth-session", updatedAt: Date.now(), authProfileOverride: "openai-codex:work", authProfileOverrideSource: "user", }; - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", @@ -1759,7 +1759,7 @@ describe("embedded attempt harness pinning", () => { providers: { openai: { baseUrl: "https://api.openai.com/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, @@ -1775,7 +1775,7 @@ describe("embedded attempt harness pinning", () => { isFallbackRetry: false, resolvedThinkLevel: "medium", timeoutMs: 1_000, - runId: "run-openai-pi-codex-oauth", + runId: "run-openai-agent-codex-oauth", opts: {} as Parameters[0]["opts"], runContext: {} as Parameters[0]["runContext"], spawnedBy: undefined, @@ -1788,11 +1788,11 @@ describe("embedded attempt harness pinning", () => { sessionHasHistory: false, }); - expectMockArgFields(runEmbeddedPiAgentMock, { + expectMockArgFields(runEmbeddedAgentMock, { provider: "openai-codex", model: "gpt-5.4", - agentHarnessId: "pi", - agentHarnessRuntimeOverride: "pi", + agentHarnessId: "openclaw", + agentHarnessRuntimeOverride: "openclaw", authProfileId: "openai-codex:work", authProfileIdSource: "user", }); @@ -1803,9 +1803,9 @@ describe("embedded attempt harness pinning", () => { sessionId: "fallback-session", updatedAt: Date.now(), }; - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: { durationMs: 1 }, - } satisfies EmbeddedPiRunResult); + } satisfies EmbeddedAgentRunResult); await runAgentAttempt({ providerOverride: "openai", @@ -1842,7 +1842,7 @@ describe("embedded attempt harness pinning", () => { }); expect(runCliAgentMock).not.toHaveBeenCalled(); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - expect(firstEmbeddedPiAgentArg()).not.toHaveProperty("agentHarnessId", "claude-cli"); + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); + expect(firstEmbeddedAgentArg()).not.toHaveProperty("agentHarnessId", "claude-cli"); }); }); diff --git a/src/agents/command/attempt-execution.ts b/src/agents/command/attempt-execution.ts index aeff99033e3..19fed9250b4 100644 --- a/src/agents/command/attempt-execution.ts +++ b/src/agents/command/attempt-execution.ts @@ -1,4 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { formatAcpErrorChain } from "../../acp/runtime/errors.js"; import type { AcpRuntimeEvent } from "../../acp/runtime/types.js"; import { normalizeReplyPayload } from "../../auto-reply/reply/normalize-reply.js"; @@ -27,14 +26,15 @@ import { ensureAuthProfileStore } from "../auth-profiles/store.js"; import { resolveBootstrapWarningSignaturesSeen } from "../bootstrap-budget.js"; import { runCliAgent } from "../cli-runner.js"; import { getCliSessionBinding, setCliSessionBinding } from "../cli-session.js"; +import { runEmbeddedAgent, type EmbeddedAgentRunResult } from "../embedded-agent.js"; import { FailoverError } from "../failover-error.js"; import { runAgentHarnessBeforeMessageWriteHook } from "../harness/hook-helpers.js"; import { resolveAvailableAgentHarnessPolicy } from "../harness/selection.js"; import { resolveCliRuntimeExecutionProvider } from "../model-runtime-aliases.js"; import { isCliProvider } from "../model-selection.js"; -import { resolveOpenAIRuntimeProviderForPi } from "../openai-codex-routing.js"; -import { runEmbeddedPiAgent, type EmbeddedPiRunResult } from "../pi-embedded.js"; +import { resolveOpenAIRuntimeProvider } from "../openai-codex-routing.js"; import { buildAgentRuntimeAuthPlan } from "../runtime-plan/auth.js"; +import type { AgentMessage } from "../runtime/index.js"; import { acquireSessionWriteLock, resolveSessionWriteLockOptions } from "../session-write-lock.js"; import { buildWorkspaceSkillSnapshot } from "../skills.js"; import { buildUsageWithNoCost } from "../stream-message-shared.js"; @@ -295,7 +295,7 @@ async function persistTextTurnTranscript( return sessionEntry; } -function resolveCliTranscriptReplyText(result: EmbeddedPiRunResult): string { +function resolveCliTranscriptReplyText(result: EmbeddedAgentRunResult): string { const visibleText = result.meta.finalAssistantVisibleText?.trim(); if (visibleText) { return visibleText; @@ -340,7 +340,7 @@ export async function persistCliTurnTranscript(params: { body: string; transcriptBody?: string; userMessage?: PersistedUserTurnMessage; - result: EmbeddedPiRunResult; + result: EmbeddedAgentRunResult; sessionId: string; sessionKey: string; sessionEntry: SessionEntry | undefined; @@ -444,7 +444,7 @@ export function runAgentAttempt(params: { ); const bootstrapPromptWarningSignature = bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1]; - const requestedAgentHarnessId = isRawModelRun ? "pi" : undefined; + const requestedAgentHarnessId = isRawModelRun ? "openclaw" : undefined; const cliExecutionProvider = isRawModelRun ? params.providerOverride : (resolveCliRuntimeExecutionProvider({ @@ -455,7 +455,7 @@ export function runAgentAttempt(params: { authProfileId: params.sessionEntry?.authProfileOverride, }) ?? params.providerOverride); const agentHarnessPolicy = isRawModelRun - ? ({ runtime: "pi" } as const) + ? ({ runtime: "openclaw" } as const) : resolveAvailableAgentHarnessPolicy({ provider: params.providerOverride, modelId: params.modelOverride, @@ -487,7 +487,7 @@ export function runAgentAttempt(params: { allowHarnessAuthProfileForwarding: !isCliProvider(cliExecutionProvider, params.cfg), }); const authProfileId = runtimeAuthPlan.forwardedAuthProfileId; - const embeddedPiProvider = resolveOpenAIRuntimeProviderForPi({ + const embeddedAgentProvider = resolveOpenAIRuntimeProvider({ provider: params.providerOverride, harnessRuntime: agentHarnessPolicy.runtime, agentHarnessId: requestedAgentHarnessId, @@ -496,10 +496,10 @@ export function runAgentAttempt(params: { config: params.cfg, workspaceDir: params.workspaceDir, }); - const embeddedPiHarnessOverride = + const embeddedAgentHarnessOverride = requestedAgentHarnessId ?? - (agentHarnessPolicy.runtime === "pi" && embeddedPiProvider !== params.providerOverride - ? "pi" + (agentHarnessPolicy.runtime === "openclaw" && embeddedAgentProvider !== params.providerOverride + ? "openclaw" : undefined); if (!isRawModelRun && isCliProvider(cliExecutionProvider, params.cfg)) { const cliSessionBinding = getCliSessionBinding(params.sessionEntry, cliExecutionProvider); @@ -646,7 +646,7 @@ export function runAgentAttempt(params: { }); } - return runEmbeddedPiAgent({ + return runEmbeddedAgent({ sessionId: params.sessionId, sessionKey: params.sessionKey, agentId: params.sessionAgentId, @@ -668,14 +668,14 @@ export function runAgentAttempt(params: { sessionFile: params.sessionFile, workspaceDir: params.workspaceDir, config: params.cfg, - agentHarnessId: embeddedPiHarnessOverride, - agentHarnessRuntimeOverride: embeddedPiHarnessOverride, + agentHarnessId: embeddedAgentHarnessOverride, + agentHarnessRuntimeOverride: embeddedAgentHarnessOverride, skillsSnapshot: params.skillsSnapshot, prompt: effectivePrompt, images: params.isFallbackRetry ? undefined : params.opts.images, imageOrder: params.isFallbackRetry ? undefined : params.opts.imageOrder, clientTools: params.opts.clientTools, - provider: embeddedPiProvider, + provider: embeddedAgentProvider, model: params.modelOverride, modelFallbacksOverride: params.modelFallbacksOverride, authProfileId, diff --git a/src/agents/command/cli-compaction.test.ts b/src/agents/command/cli-compaction.test.ts index 879c3d555e2..9f80dcd28d5 100644 --- a/src/agents/command/cli-compaction.test.ts +++ b/src/agents/command/cli-compaction.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent"; +import { CURRENT_SESSION_VERSION } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { SessionEntry } from "../../config/sessions/types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; @@ -114,7 +114,7 @@ describe("runCliTurnCompactionLifecycle", () => { const maintenance = vi.fn(async () => ({ changed: false, bytesFreed: 0, rewrittenEntries: 0 })); setCliCompactionTestDeps({ resolveContextEngine: async () => buildContextEngine({ compactCalls }), - createPreparedEmbeddedPiSettingsManager: async () => ({ + createPreparedEmbeddedAgentSettingsManager: async () => ({ getCompactionReserveTokens: () => 200, getCompactionKeepRecentTokens: () => 0, applyOverrides: () => {}, @@ -177,7 +177,7 @@ describe("runCliTurnCompactionLifecycle", () => { expect(updatedEntry?.claudeCliSessionId).toBeUndefined(); }); - it("skips OpenClaw automatic CLI compaction for OpenAI Codex runtime sessions", async () => { + it("routes OpenAI Codex harness CLI compaction through native harness compaction", async () => { const sessionKey = "agent:main:codex"; const sessionId = "session-codex"; const sessionFile = path.join(tmpDir, "session-codex.jsonl"); @@ -197,14 +197,15 @@ describe("runCliTurnCompactionLifecycle", () => { await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); const compactCalls: Array[0]> = []; - const resolveContextEngine = vi.fn(async () => buildContextEngine({ compactCalls })); + const contextEngine = buildContextEngine({ compactCalls }); + const resolveContextEngine = vi.fn(async () => contextEngine); const ensureSelectedAgentHarnessPlugin = vi.fn(async () => undefined); const compactAgentHarnessSession = vi.fn(async () => ({ ok: true, compacted: true, result: { tokensBefore: 950, tokensAfter: 100 }, })); - const applyPiAutoCompactionGuard = vi.fn(async () => ({ + const applyAgentAutoCompactionGuard = vi.fn(async () => ({ supported: true, disabled: false, })); @@ -216,7 +217,7 @@ describe("runCliTurnCompactionLifecycle", () => { resolveContextEngine, ensureSelectedAgentHarnessPlugin, maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, - createPreparedEmbeddedPiSettingsManager: async () => ({ + createPreparedEmbeddedAgentSettingsManager: async () => ({ getCompactionReserveTokens: () => 200, getCompactionKeepRecentTokens: () => 0, applyOverrides: () => {}, @@ -231,7 +232,7 @@ describe("runCliTurnCompactionLifecycle", () => { effectiveReserveTokens: 200, }), resolveLiveToolResultMaxChars: () => 20_000, - applyPiAutoCompactionGuard, + applyAgentAutoCompactionGuard, recordCliCompactionInStore, }); @@ -249,72 +250,56 @@ describe("runCliTurnCompactionLifecycle", () => { model: "gpt-5.5", }); - expect(resolveContextEngine).not.toHaveBeenCalled(); - expect(applyPiAutoCompactionGuard).not.toHaveBeenCalled(); - expect(ensureSelectedAgentHarnessPlugin).not.toHaveBeenCalled(); - expect(compactAgentHarnessSession).not.toHaveBeenCalled(); - expect(compactCalls).toHaveLength(0); - expect(recordCliCompactionInStore).not.toHaveBeenCalled(); - expect(updatedEntry).toBe(sessionEntry); - }); - - it("skips OpenClaw automatic CLI compaction when OpenAI resolves to Codex by policy", async () => { - const sessionKey = "agent:main:codex-policy"; - const sessionId = "session-codex-policy"; - const sessionFile = path.join(tmpDir, "session-codex-policy.jsonl"); - const storePath = path.join(tmpDir, "sessions-codex-policy.json"); - await writeSessionFile({ sessionFile, sessionId }); - - const sessionEntry: SessionEntry = { - sessionId, - updatedAt: Date.now(), - sessionFile, - contextTokens: 1_000, - totalTokens: 950, - totalTokensFresh: true, - }; - const sessionStore: Record = { [sessionKey]: sessionEntry }; - await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); - - const openSessionManager = vi.fn(() => { - throw new Error("OpenClaw must not inspect Codex transcripts for automatic compaction"); - }); - const resolveContextEngine = vi.fn(); - const ensureSelectedAgentHarnessPlugin = vi.fn(); - const compactAgentHarnessSession = vi.fn(); - setCliCompactionTestDeps({ - openSessionManager: openSessionManager as never, - resolveContextEngine: resolveContextEngine as never, - ensureSelectedAgentHarnessPlugin: ensureSelectedAgentHarnessPlugin as never, - maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, - }); - - const updatedEntry = await runCliTurnCompactionLifecycle({ - cfg: {} as OpenClawConfig, + expect(resolveContextEngine).toHaveBeenCalledTimes(1); + expect(applyAgentAutoCompactionGuard).toHaveBeenCalledWith( + expect.objectContaining({ + contextEngineInfo: contextEngine.info, + }), + ); + expect(ensureSelectedAgentHarnessPlugin).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "openai", + modelId: "gpt-5.5", + sessionKey, + agentHarnessRuntimeOverride: "codex", + }), + ); + expect(applyAgentAutoCompactionGuard.mock.invocationCallOrder[0] ?? 0).toBeLessThan( + compactAgentHarnessSession.mock.invocationCallOrder[0] ?? 0, + ); + expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); + const compactAgentHarnessSessionCalls = compactAgentHarnessSession.mock + .calls as unknown as Array<[Record]>; + expect(compactAgentHarnessSessionCalls[0]?.[0]).toMatchObject({ sessionId, sessionKey, - sessionEntry, - sessionStore, - storePath, - sessionAgentId: "main", - workspaceDir: tmpDir, - agentDir: tmpDir, + sessionFile, provider: "openai", model: "gpt-5.5", + contextTokenBudget: 1_000, + currentTokenCount: 950, + contextEngine, + agentHarnessId: "codex", + trigger: "budget", + force: true, }); - - expect(openSessionManager).not.toHaveBeenCalled(); - expect(resolveContextEngine).not.toHaveBeenCalled(); - expect(ensureSelectedAgentHarnessPlugin).not.toHaveBeenCalled(); - expect(compactAgentHarnessSession).not.toHaveBeenCalled(); - expect(updatedEntry).toBe(sessionEntry); + expect(compactCalls).toHaveLength(0); + expect(recordCliCompactionInStore).toHaveBeenCalledTimes(1); + expect(recordCliCompactionInStore).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "openai", + sessionKey, + tokensAfter: 100, + }), + ); + expect(updatedEntry?.compactionCount).toBe(1); }); it("ignores stale native harness ids when the active provider no longer matches", async () => { - const sessionKey = "agent:main:pi-after-codex"; - const sessionId = "session-pi-after-codex"; - const sessionFile = path.join(tmpDir, "session-pi-after-codex.jsonl"); - const storePath = path.join(tmpDir, "sessions-pi-after-codex.json"); + const sessionKey = "agent:main:openclaw-after-codex"; + const sessionId = "session-openclaw-after-codex"; + const sessionFile = path.join(tmpDir, "session-openclaw-after-codex.jsonl"); + const storePath = path.join(tmpDir, "sessions-openclaw-after-codex.json"); await writeSessionFile({ sessionFile, sessionId }); const sessionEntry: SessionEntry = { @@ -334,7 +319,7 @@ describe("runCliTurnCompactionLifecycle", () => { setCliCompactionTestDeps({ resolveContextEngine: async () => buildContextEngine({ compactCalls }), maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, - createPreparedEmbeddedPiSettingsManager: async () => ({ + createPreparedEmbeddedAgentSettingsManager: async () => ({ getCompactionReserveTokens: () => 200, getCompactionKeepRecentTokens: () => 0, applyOverrides: () => {}, @@ -366,7 +351,7 @@ describe("runCliTurnCompactionLifecycle", () => { sessionAgentId: "main", workspaceDir: tmpDir, agentDir: tmpDir, - provider: "pi", + provider: "openclaw", model: "sonnet-4.6", }); @@ -374,6 +359,240 @@ describe("runCliTurnCompactionLifecycle", () => { expect(compactCalls).toHaveLength(1); }); + it("surfaces nonrecoverable native harness CLI compaction failures", async () => { + const sessionKey = "agent:main:codex-native-failure"; + const sessionId = "session-codex-native-failure"; + const sessionFile = path.join(tmpDir, "session-codex-native-failure.jsonl"); + const storePath = path.join(tmpDir, "sessions-codex-native-failure.json"); + await writeSessionFile({ sessionFile, sessionId }); + + const sessionEntry: SessionEntry = { + sessionId, + updatedAt: Date.now(), + sessionFile, + contextTokens: 1_000, + totalTokens: 950, + totalTokensFresh: true, + agentHarnessId: "codex", + }; + const sessionStore: Record = { [sessionKey]: sessionEntry }; + await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); + + const compactCalls: Array[0]> = []; + const ensureSelectedAgentHarnessPlugin = vi.fn(async () => undefined); + const compactAgentHarnessSession = vi.fn(async () => ({ + ok: false, + compacted: false, + reason: "timed out waiting for codex app-server compaction", + })); + const recordCliCompactionInStore = vi.fn(); + setCliCompactionTestDeps({ + resolveContextEngine: async () => buildContextEngine({ compactCalls }), + ensureSelectedAgentHarnessPlugin, + maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, + createPreparedEmbeddedAgentSettingsManager: async () => ({ + getCompactionReserveTokens: () => 200, + getCompactionKeepRecentTokens: () => 0, + applyOverrides: () => {}, + }), + shouldPreemptivelyCompactBeforePrompt: () => ({ + route: "fits", + shouldCompact: false, + estimatedPromptTokens: 600, + promptBudgetBeforeReserve: 800, + overflowTokens: 0, + toolResultReducibleChars: 0, + effectiveReserveTokens: 200, + }), + resolveLiveToolResultMaxChars: () => 20_000, + recordCliCompactionInStore, + }); + + await expect( + runCliTurnCompactionLifecycle({ + cfg: {} as OpenClawConfig, + sessionId, + sessionKey, + sessionEntry, + sessionStore, + storePath, + sessionAgentId: "main", + workspaceDir: tmpDir, + agentDir: tmpDir, + provider: "codex", + model: "gpt-5.5", + }), + ).rejects.toThrow( + "CLI native harness compaction failed for codex/gpt-5.5: timed out waiting for codex app-server compaction", + ); + + expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); + expect(compactCalls).toHaveLength(0); + expect(recordCliCompactionInStore).not.toHaveBeenCalled(); + }); + + it("does not fall back when native harness compaction returns no result", async () => { + const sessionKey = "agent:main:codex-native-empty"; + const sessionId = "session-codex-native-empty"; + const sessionFile = path.join(tmpDir, "session-codex-native-empty.jsonl"); + const storePath = path.join(tmpDir, "sessions-codex-native-empty.json"); + await writeSessionFile({ sessionFile, sessionId }); + + const sessionEntry: SessionEntry = { + sessionId, + updatedAt: Date.now(), + sessionFile, + contextTokens: 1_000, + totalTokens: 950, + totalTokensFresh: true, + agentHarnessId: "codex", + }; + const sessionStore: Record = { [sessionKey]: sessionEntry }; + await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); + + const compactCalls: Array[0]> = []; + setCliCompactionTestDeps({ + resolveContextEngine: async () => buildContextEngine({ compactCalls }), + ensureSelectedAgentHarnessPlugin: vi.fn(async () => undefined), + maybeCompactAgentHarnessSession: vi.fn(async () => undefined) as never, + createPreparedEmbeddedAgentSettingsManager: async () => ({ + getCompactionReserveTokens: () => 200, + getCompactionKeepRecentTokens: () => 0, + applyOverrides: () => {}, + }), + shouldPreemptivelyCompactBeforePrompt: () => ({ + route: "fits", + shouldCompact: false, + estimatedPromptTokens: 600, + promptBudgetBeforeReserve: 800, + overflowTokens: 0, + toolResultReducibleChars: 0, + effectiveReserveTokens: 200, + }), + resolveLiveToolResultMaxChars: () => 20_000, + }); + + await expect( + runCliTurnCompactionLifecycle({ + cfg: {} as OpenClawConfig, + sessionId, + sessionKey, + sessionEntry, + sessionStore, + storePath, + sessionAgentId: "main", + workspaceDir: tmpDir, + agentDir: tmpDir, + provider: "codex", + model: "gpt-5.5", + }), + ).rejects.toThrow( + "CLI native harness compaction failed for codex/gpt-5.5: native harness compaction did not reduce context", + ); + expect(compactCalls).toHaveLength(0); + }); + + it("passes owning context engines into native harness CLI compaction", async () => { + const sessionKey = "agent:main:codex-owned-engine"; + const sessionId = "session-codex-owned-engine"; + const sessionFile = path.join(tmpDir, "session-codex-owned-engine.jsonl"); + const storePath = path.join(tmpDir, "sessions-codex-owned-engine.json"); + await writeSessionFile({ sessionFile, sessionId }); + + const sessionEntry: SessionEntry = { + sessionId, + updatedAt: Date.now(), + sessionFile, + contextTokens: 1_000, + totalTokens: 950, + totalTokensFresh: true, + agentHarnessId: "codex", + }; + const sessionStore: Record = { [sessionKey]: sessionEntry }; + await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); + + const compactCalls: Array[0]> = []; + const contextEngine = { + ...buildContextEngine({ compactCalls }), + info: { + id: "lossless-claw", + name: "Lossless Claw", + ownsCompaction: true, + }, + } satisfies ContextEngine; + const ensureSelectedAgentHarnessPlugin = vi.fn(async () => undefined); + const compactAgentHarnessSession = vi.fn(async (compactParams) => { + expect(compactParams.contextEngine).toBe(contextEngine); + expect(compactParams.contextEngineRuntimeContext).toMatchObject({ + currentTokenCount: 950, + tokenBudget: 1_000, + trigger: "cli_native_budget", + }); + return { + ok: true, + compacted: true, + result: { + summary: "engine-owned", + firstKeptEntryId: "entry-1", + tokensBefore: 950, + tokensAfter: 42, + sessionId: "session-codex-owned-engine-rotated", + sessionFile: path.join(tmpDir, "session-codex-owned-engine-rotated.jsonl"), + }, + }; + }); + const recordCliCompactionInStore = vi.fn(async () => ({ + ...sessionEntry, + compactionCount: 1, + })); + setCliCompactionTestDeps({ + resolveContextEngine: async () => contextEngine, + ensureSelectedAgentHarnessPlugin, + maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, + createPreparedEmbeddedAgentSettingsManager: async () => ({ + getCompactionReserveTokens: () => 200, + getCompactionKeepRecentTokens: () => 0, + applyOverrides: () => {}, + }), + shouldPreemptivelyCompactBeforePrompt: () => ({ + route: "fits", + shouldCompact: false, + estimatedPromptTokens: 600, + promptBudgetBeforeReserve: 800, + overflowTokens: 0, + toolResultReducibleChars: 0, + effectiveReserveTokens: 200, + }), + resolveLiveToolResultMaxChars: () => 20_000, + recordCliCompactionInStore, + }); + + await runCliTurnCompactionLifecycle({ + cfg: {} as OpenClawConfig, + sessionId, + sessionKey, + sessionEntry, + sessionStore, + storePath, + sessionAgentId: "main", + workspaceDir: tmpDir, + agentDir: tmpDir, + provider: "codex", + model: "gpt-5.5", + }); + + expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); + expect(recordCliCompactionInStore).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "codex", + sessionKey, + tokensAfter: 42, + newSessionId: "session-codex-owned-engine-rotated", + newSessionFile: path.join(tmpDir, "session-codex-owned-engine-rotated.jsonl"), + }), + ); + }); + it("falls back to context-engine compaction when a pinned harness has no native compactor", async () => { const sessionKey = "agent:main:external-harness"; const sessionId = "session-external-harness"; @@ -410,7 +629,7 @@ describe("runCliTurnCompactionLifecycle", () => { resolveContextEngine: async () => buildContextEngine({ compactCalls }), ensureSelectedAgentHarnessPlugin, maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, - createPreparedEmbeddedPiSettingsManager: async () => ({ + createPreparedEmbeddedAgentSettingsManager: async () => ({ getCompactionReserveTokens: () => 200, getCompactionKeepRecentTokens: () => 0, applyOverrides: () => {}, @@ -456,11 +675,11 @@ describe("runCliTurnCompactionLifecycle", () => { expect(updatedEntry?.compactionCount).toBe(1); }); - it("keeps successful context-engine fallback when post-compaction maintenance fails", async () => { - const sessionKey = "agent:main:external-harness-stale-maintenance"; - const sessionId = "session-external-harness-stale-maintenance"; - const sessionFile = path.join(tmpDir, "session-external-harness-stale-maintenance.jsonl"); - const storePath = path.join(tmpDir, "sessions-external-harness-stale-maintenance.json"); + it("falls back to context-engine compaction when Codex native binding is stale", async () => { + const sessionKey = "agent:main:codex-stale-binding"; + const sessionId = "session-codex-stale-binding"; + const sessionFile = path.join(tmpDir, "session-codex-stale-binding.jsonl"); + const storePath = path.join(tmpDir, "sessions-codex-stale-binding.json"); await writeSessionFile({ sessionFile, sessionId }); const sessionEntry: SessionEntry = { @@ -470,29 +689,31 @@ describe("runCliTurnCompactionLifecycle", () => { contextTokens: 1_000, totalTokens: 950, totalTokensFresh: true, - agentHarnessId: "external-harness", + agentHarnessId: "codex", }; const sessionStore: Record = { [sessionKey]: sessionEntry }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); const compactCalls: Array[0]> = []; - const maintenance = vi.fn(async () => { - throw new Error("maintenance rotated stale binding"); - }); + const ensureSelectedAgentHarnessPlugin = vi.fn(async () => undefined); + const compactAgentHarnessSession = vi.fn(async () => ({ + ok: false, + compacted: false, + reason: "thread not found: thread-1", + failure: { + reason: "stale_thread_binding", + }, + })); + const maintenance = vi.fn(async () => ({ changed: false, bytesFreed: 0, rewrittenEntries: 0 })); const recordCliCompactionInStore = vi.fn(async () => ({ ...sessionEntry, compactionCount: 1, })); setCliCompactionTestDeps({ resolveContextEngine: async () => buildContextEngine({ compactCalls }), - ensureSelectedAgentHarnessPlugin: vi.fn(async () => undefined), - maybeCompactAgentHarnessSession: vi.fn(async () => ({ - ok: false, - compacted: false, - reason: "thread not found: thread-1", - failure: { reason: "stale_thread_binding" }, - })) as never, - createPreparedEmbeddedPiSettingsManager: async () => ({ + ensureSelectedAgentHarnessPlugin, + maybeCompactAgentHarnessSession: compactAgentHarnessSession as never, + createPreparedEmbeddedAgentSettingsManager: async () => ({ getCompactionReserveTokens: () => 200, getCompactionKeepRecentTokens: () => 0, applyOverrides: () => {}, @@ -521,14 +742,96 @@ describe("runCliTurnCompactionLifecycle", () => { sessionAgentId: "main", workspaceDir: tmpDir, agentDir: tmpDir, - provider: "external-harness", - model: "model", + provider: "codex", + model: "gpt-5.5", + }); + + expect(compactAgentHarnessSession).toHaveBeenCalledTimes(1); + expect(compactCalls).toHaveLength(1); + expect(maintenance).toHaveBeenCalledTimes(1); + expect(recordCliCompactionInStore).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "codex", + sessionKey, + tokensAfter: undefined, + }), + ); + expect(updatedEntry?.compactionCount).toBe(1); + }); + + it("keeps successful context-engine fallback when post-compaction maintenance fails", async () => { + const sessionKey = "agent:main:codex-stale-maintenance"; + const sessionId = "session-codex-stale-maintenance"; + const sessionFile = path.join(tmpDir, "session-codex-stale-maintenance.jsonl"); + const storePath = path.join(tmpDir, "sessions-codex-stale-maintenance.json"); + await writeSessionFile({ sessionFile, sessionId }); + + const sessionEntry: SessionEntry = { + sessionId, + updatedAt: Date.now(), + sessionFile, + contextTokens: 1_000, + totalTokens: 950, + totalTokensFresh: true, + agentHarnessId: "codex", + }; + const sessionStore: Record = { [sessionKey]: sessionEntry }; + await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2), "utf-8"); + + const compactCalls: Array[0]> = []; + const maintenance = vi.fn(async () => { + throw new Error("maintenance rotated stale binding"); + }); + const recordCliCompactionInStore = vi.fn(async () => ({ + ...sessionEntry, + compactionCount: 1, + })); + setCliCompactionTestDeps({ + resolveContextEngine: async () => buildContextEngine({ compactCalls }), + ensureSelectedAgentHarnessPlugin: vi.fn(async () => undefined), + maybeCompactAgentHarnessSession: vi.fn(async () => ({ + ok: false, + compacted: false, + reason: "thread not found: thread-1", + failure: { reason: "stale_thread_binding" }, + })) as never, + createPreparedEmbeddedAgentSettingsManager: async () => ({ + getCompactionReserveTokens: () => 200, + getCompactionKeepRecentTokens: () => 0, + applyOverrides: () => {}, + }), + shouldPreemptivelyCompactBeforePrompt: () => ({ + route: "fits", + shouldCompact: false, + estimatedPromptTokens: 600, + promptBudgetBeforeReserve: 800, + overflowTokens: 0, + toolResultReducibleChars: 0, + effectiveReserveTokens: 200, + }), + resolveLiveToolResultMaxChars: () => 20_000, + runContextEngineMaintenance: maintenance, + recordCliCompactionInStore, + }); + + const updatedEntry = await runCliTurnCompactionLifecycle({ + cfg: {} as OpenClawConfig, + sessionId, + sessionKey, + sessionEntry, + sessionStore, + storePath, + sessionAgentId: "main", + workspaceDir: tmpDir, + agentDir: tmpDir, + provider: "codex", + model: "gpt-5.5", }); expect(compactCalls).toHaveLength(1); expect(maintenance).toHaveBeenCalledTimes(1); expect(recordCliCompactionInStore).toHaveBeenCalledWith( - expect.objectContaining({ provider: "external-harness", sessionKey }), + expect.objectContaining({ provider: "codex", sessionKey }), ); expect(updatedEntry?.compactionCount).toBe(1); }); @@ -556,7 +859,7 @@ describe("runCliTurnCompactionLifecycle", () => { calls.push("resolve"); return buildContextEngine({ compactCalls: [] }); }, - createPreparedEmbeddedPiSettingsManager: async () => ({ + createPreparedEmbeddedAgentSettingsManager: async () => ({ getCompactionReserveTokens: () => 200, getCompactionKeepRecentTokens: () => 0, applyOverrides: () => {}, @@ -624,7 +927,7 @@ describe("runCliTurnCompactionLifecycle", () => { return await new Promise(() => {}); }, }), - createPreparedEmbeddedPiSettingsManager: async () => ({ + createPreparedEmbeddedAgentSettingsManager: async () => ({ getCompactionReserveTokens: () => 200, getCompactionKeepRecentTokens: () => 0, applyOverrides: () => {}, diff --git a/src/agents/command/cli-compaction.ts b/src/agents/command/cli-compaction.ts index b9f2b705aa5..c8e7cc98292 100644 --- a/src/agents/command/cli-compaction.ts +++ b/src/agents/command/cli-compaction.ts @@ -1,5 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; import type { SessionEntry } from "../../config/sessions/types.js"; import type { AgentCompactionMode } from "../../config/types.agent-defaults.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; @@ -7,24 +5,27 @@ import { ensureContextEnginesInitialized as ensureContextEnginesInitializedImpl import { resolveContextEngine as resolveContextEngineImpl } from "../../context-engine/registry.js"; import type { ContextEngine } from "../../context-engine/types.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; -import { resolveAgentHarnessPolicy } from "../harness/policy.js"; -import { ensureSelectedAgentHarnessPlugin as ensureSelectedAgentHarnessPluginImpl } from "../harness/runtime-plugin.js"; -import { maybeCompactAgentHarnessSession as maybeCompactAgentHarnessSessionImpl } from "../harness/selection.js"; -import { buildEmbeddedCompactionRuntimeContext } from "../pi-embedded-runner/compaction-runtime-context.js"; +import { createPreparedEmbeddedAgentSettingsManager as createPreparedEmbeddedAgentSettingsManagerImpl } from "../agent-project-settings.js"; +import { OPENCLAW_AGENT_RUNTIME_ID } from "../agent-runtime-id.js"; +import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js"; +import { + applyAgentAutoCompactionGuard as applyAgentAutoCompactionGuardImpl, + resolveEffectiveCompactionMode, +} from "../agent-settings.js"; +import { buildEmbeddedCompactionRuntimeContext } from "../embedded-agent-runner/compaction-runtime-context.js"; import { compactContextEngineWithSafetyTimeout, compactWithSafetyTimeout, resolveCompactionTimeoutMs, -} from "../pi-embedded-runner/compaction-safety-timeout.js"; -import { runContextEngineMaintenance as runContextEngineMaintenanceImpl } from "../pi-embedded-runner/context-engine-maintenance.js"; -import { shouldPreemptivelyCompactBeforePrompt as shouldPreemptivelyCompactBeforePromptImpl } from "../pi-embedded-runner/run/preemptive-compaction.js"; -import { resolveLiveToolResultMaxChars as resolveLiveToolResultMaxCharsImpl } from "../pi-embedded-runner/tool-result-truncation.js"; -import type { EmbeddedPiCompactResult } from "../pi-embedded-runner/types.js"; -import { createPreparedEmbeddedPiSettingsManager as createPreparedEmbeddedPiSettingsManagerImpl } from "../pi-project-settings.js"; -import { - applyPiAutoCompactionGuard as applyPiAutoCompactionGuardImpl, - resolveEffectiveCompactionMode, -} from "../pi-settings.js"; +} from "../embedded-agent-runner/compaction-safety-timeout.js"; +import { runContextEngineMaintenance as runContextEngineMaintenanceImpl } from "../embedded-agent-runner/context-engine-maintenance.js"; +import { shouldPreemptivelyCompactBeforePrompt as shouldPreemptivelyCompactBeforePromptImpl } from "../embedded-agent-runner/run/preemptive-compaction.js"; +import { resolveLiveToolResultMaxChars as resolveLiveToolResultMaxCharsImpl } from "../embedded-agent-runner/tool-result-truncation.js"; +import type { EmbeddedAgentCompactResult } from "../embedded-agent-runner/types.js"; +import { ensureSelectedAgentHarnessPlugin as ensureSelectedAgentHarnessPluginImpl } from "../harness/runtime-plugin.js"; +import { maybeCompactAgentHarnessSession as maybeCompactAgentHarnessSessionImpl } from "../harness/selection.js"; +import type { AgentMessage } from "../runtime/index.js"; +import { SessionManager } from "../sessions/index.js"; import type { SkillSnapshot } from "../skills.js"; import { recordCliCompactionInStore as recordCliCompactionInStoreImpl } from "./session-store.js"; @@ -44,13 +45,13 @@ type CliCompactionDeps = { openSessionManager: (sessionFile: string) => SessionManagerLike; ensureContextEnginesInitialized: () => void; resolveContextEngine: (cfg: OpenClawConfig) => Promise; - createPreparedEmbeddedPiSettingsManager: (params: { + createPreparedEmbeddedAgentSettingsManager: (params: { cwd: string; agentDir: string; cfg?: OpenClawConfig; contextTokenBudget?: number; }) => SettingsManagerLike | Promise; - applyPiAutoCompactionGuard: (params: { + applyAgentAutoCompactionGuard: (params: { settingsManager: SettingsManagerLike; contextEngineInfo?: ContextEngine["info"]; compactionMode?: AgentCompactionMode; @@ -65,7 +66,7 @@ type CliCompactionDeps = { type NativeHarnessCliCompactionOutcome = { compacted: boolean; - result?: EmbeddedPiCompactResult; + result?: EmbeddedAgentCompactResult; fallbackToContextEngine?: boolean; failureReason?: string; }; @@ -97,8 +98,8 @@ const cliCompactionDeps: CliCompactionDeps = { openSessionManager: (sessionFile: string) => SessionManager.open(sessionFile), ensureContextEnginesInitialized: ensureContextEnginesInitializedImpl, resolveContextEngine: resolveContextEngineImpl, - createPreparedEmbeddedPiSettingsManager: createPreparedEmbeddedPiSettingsManagerImpl, - applyPiAutoCompactionGuard: applyPiAutoCompactionGuardImpl, + createPreparedEmbeddedAgentSettingsManager: createPreparedEmbeddedAgentSettingsManagerImpl, + applyAgentAutoCompactionGuard: applyAgentAutoCompactionGuardImpl, shouldPreemptivelyCompactBeforePrompt: shouldPreemptivelyCompactBeforePromptImpl, resolveLiveToolResultMaxChars: resolveLiveToolResultMaxCharsImpl, runContextEngineMaintenance: runContextEngineMaintenanceImpl, @@ -116,8 +117,8 @@ export function resetCliCompactionTestDeps(): void { openSessionManager: (sessionFile: string) => SessionManager.open(sessionFile), ensureContextEnginesInitialized: ensureContextEnginesInitializedImpl, resolveContextEngine: resolveContextEngineImpl, - createPreparedEmbeddedPiSettingsManager: createPreparedEmbeddedPiSettingsManagerImpl, - applyPiAutoCompactionGuard: applyPiAutoCompactionGuardImpl, + createPreparedEmbeddedAgentSettingsManager: createPreparedEmbeddedAgentSettingsManagerImpl, + applyAgentAutoCompactionGuard: applyAgentAutoCompactionGuardImpl, shouldPreemptivelyCompactBeforePrompt: shouldPreemptivelyCompactBeforePromptImpl, resolveLiveToolResultMaxChars: resolveLiveToolResultMaxCharsImpl, runContextEngineMaintenance: runContextEngineMaintenanceImpl, @@ -155,7 +156,7 @@ function isNativeHarnessCompactionSession( provider: string, ): sessionEntry is SessionEntry { const harnessId = sessionEntry?.agentHarnessId?.trim().toLowerCase(); - if (!harnessId || harnessId === "pi") { + if (!harnessId || normalizeOptionalAgentRuntimeId(harnessId) === OPENCLAW_AGENT_RUNTIME_ID) { return false; } const providerId = provider.trim().toLowerCase(); @@ -167,13 +168,13 @@ function isNativeHarnessCompactionSession( } function isUnsupportedNativeHarnessCompaction( - result: EmbeddedPiCompactResult | undefined, + result: EmbeddedAgentCompactResult | undefined, ): boolean { return result?.ok === false && result.failure?.reason === "unsupported_harness_compaction"; } function isRecoverableNativeHarnessCompactionFailure( - result: EmbeddedPiCompactResult | undefined, + result: EmbeddedAgentCompactResult | undefined, ): boolean { return ( result?.ok === false && @@ -182,45 +183,6 @@ function isRecoverableNativeHarnessCompactionFailure( ); } -function isCodexNativeHarnessCompactionSession( - sessionEntry: SessionEntry, - provider: string, -): boolean { - const harnessId = sessionEntry.agentHarnessId?.trim().toLowerCase(); - const providerId = provider.trim().toLowerCase(); - return ( - harnessId === "codex" && - (providerId === "codex" || providerId === "openai" || providerId === "openai-codex") - ); -} - -function shouldSkipAutomaticCompactionForCodexRuntime(params: { - cfg: OpenClawConfig; - sessionEntry: SessionEntry; - sessionAgentId: string; - sessionKey: string; - provider: string; - model: string; -}): boolean { - const runtimeOverride = params.sessionEntry.agentRuntimeOverride?.trim().toLowerCase(); - if (runtimeOverride && runtimeOverride !== "auto" && runtimeOverride !== "default") { - return runtimeOverride === "codex"; - } - const harnessId = params.sessionEntry.agentHarnessId?.trim().toLowerCase(); - if (harnessId) { - return isCodexNativeHarnessCompactionSession(params.sessionEntry, params.provider); - } - return ( - resolveAgentHarnessPolicy({ - provider: params.provider, - modelId: params.model, - config: params.cfg, - agentId: params.sessionAgentId, - sessionKey: params.sessionKey, - }).runtime === "codex" - ); -} - function readAgentIdFromSessionKey(sessionKey: string): string | undefined { const parts = sessionKey.trim().split(":"); return parts[0] === "agent" && parts[1]?.trim() ? parts[1].trim() : undefined; @@ -367,7 +329,7 @@ async function compactNativeHarnessCliTranscript(params: { thinkLevel?: Parameters[0]["thinkLevel"]; extraSystemPrompt?: string; }): Promise { - let result: EmbeddedPiCompactResult | undefined; + let result: EmbeddedAgentCompactResult | undefined; try { const sessionAgentId = readAgentIdFromSessionKey(params.sessionKey); const nativeHarnessId = params.sessionEntry.agentHarnessId?.trim(); @@ -441,9 +403,8 @@ async function compactNativeHarnessCliTranscript(params: { if (!result?.compacted) { const fallbackToContextEngine = - !isCodexNativeHarnessCompactionSession(params.sessionEntry, params.provider) && - (isUnsupportedNativeHarnessCompaction(result) || - isRecoverableNativeHarnessCompactionFailure(result)); + isUnsupportedNativeHarnessCompaction(result) || + isRecoverableNativeHarnessCompactionFailure(result); log.warn( `CLI native harness compaction did not reduce context for ${params.provider}/${params.model}: ${result?.reason ?? "nothing to compact"}`, ); @@ -476,36 +437,14 @@ export async function runCliTurnCompactionLifecycle(params: { thinkLevel?: Parameters[0]["thinkLevel"]; extraSystemPrompt?: string; }): Promise { - const sessionEntry = params.sessionEntry; - const sessionFile = sessionEntry?.sessionFile; - const contextTokenBudget = resolvePositiveInteger(sessionEntry?.contextTokens); + const sessionFile = params.sessionEntry?.sessionFile; + const contextTokenBudget = resolvePositiveInteger(params.sessionEntry?.contextTokens); if (!sessionFile || !contextTokenBudget) { - return sessionEntry; - } - if ( - shouldSkipAutomaticCompactionForCodexRuntime({ - cfg: params.cfg, - sessionEntry, - sessionAgentId: params.sessionAgentId, - sessionKey: params.sessionKey, - provider: params.provider, - model: params.model, - }) - ) { - // Codex CLI/app-server runtimes own their automatic transcript compaction. - // Avoid resurrecting OpenClaw's paternalistic budget fallback here; explicit - // /compact or plugin compaction still forwards through the harness path. - log.debug("skipping OpenClaw CLI compaction for Codex runtime session", { - sessionId: params.sessionId, - sessionKey: params.sessionKey, - provider: params.provider, - model: params.model, - }); - return sessionEntry; + return params.sessionEntry; } const sessionManager = cliCompactionDeps.openSessionManager(sessionFile); - const settingsManager = await cliCompactionDeps.createPreparedEmbeddedPiSettingsManager({ + const settingsManager = await cliCompactionDeps.createPreparedEmbeddedAgentSettingsManager({ cwd: params.workspaceDir, agentDir: params.agentDir, cfg: params.cfg, @@ -536,7 +475,7 @@ export async function runCliTurnCompactionLifecycle(params: { } let compacted = false; - let nativeCompactionResult: EmbeddedPiCompactResult | undefined; + let nativeCompactionResult: EmbeddedAgentCompactResult | undefined; let useContextEngineCompaction = true; let nativeFallbackToContextEngine = false; let resolvedContextEngine: ContextEngine | undefined; @@ -546,7 +485,7 @@ export async function runCliTurnCompactionLifecycle(params: { return; } autoCompactionGuardApplied = true; - await cliCompactionDeps.applyPiAutoCompactionGuard({ + await cliCompactionDeps.applyAgentAutoCompactionGuard({ settingsManager, contextEngineInfo: contextEngine.info, compactionMode: resolveEffectiveCompactionMode(params.cfg), diff --git a/src/agents/command/delivery.ts b/src/agents/command/delivery.ts index ba8a67aecc8..339736b030e 100644 --- a/src/agents/command/delivery.ts +++ b/src/agents/command/delivery.ts @@ -29,12 +29,12 @@ import { import type { OutboundSessionContext } from "../../infra/outbound/session-context.js"; import { type RuntimeEnv, writeRuntimeJson } from "../../runtime.js"; import { isInternalMessageChannel } from "../../utils/message-channel.js"; +import type { MessagingToolSend } from "../embedded-agent-messaging.types.js"; +import type { EmbeddedAgentRunMeta } from "../embedded-agent-runner/types.js"; import { isNestedAgentLane } from "../lanes.js"; -import type { MessagingToolSend } from "../pi-embedded-messaging.types.js"; -import type { EmbeddedPiRunMeta } from "../pi-embedded-runner/types.js"; import type { AgentCommandOpts, AgentCommandResultMetaOverrides } from "./types.js"; -type RunResult = Awaited>; +type RunResult = Awaited>; type DurableSendResult = Awaited>; export type AgentCommandDeliveryPayloadStatus = "sent" | "suppressed" | "failed"; @@ -70,7 +70,7 @@ export type AgentCommandDeliveryStatus = { export type AgentCommandDeliveryResult = { payloads: ReturnType; - meta: EmbeddedPiRunMeta & AgentCommandResultMetaOverrides; + meta: EmbeddedAgentRunMeta & AgentCommandResultMetaOverrides; didSendViaMessagingTool?: boolean; messagingToolSentTexts?: string[]; messagingToolSentMediaUrls?: string[]; @@ -155,9 +155,9 @@ function logNestedOutput( } function mergeResultMetaOverrides( - meta: EmbeddedPiRunMeta, + meta: EmbeddedAgentRunMeta, overrides: AgentCommandResultMetaOverrides | undefined, -): EmbeddedPiRunMeta & AgentCommandResultMetaOverrides { +): EmbeddedAgentRunMeta & AgentCommandResultMetaOverrides { if (!overrides) { return meta; } diff --git a/src/agents/command/session-store.test.ts b/src/agents/command/session-store.test.ts index bfc24ac22e5..176bbae6186 100644 --- a/src/agents/command/session-store.test.ts +++ b/src/agents/command/session-store.test.ts @@ -5,7 +5,7 @@ import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; import { loadSessionStore } from "../../config/sessions.js"; -import type { EmbeddedPiRunResult } from "../pi-embedded.js"; +import type { EmbeddedAgentRunResult } from "../embedded-agent.js"; import { clearCliSessionInStore, recordCliCompactionInStore, @@ -169,7 +169,7 @@ describe("updateSessionStoreAfterAgentRun", () => { }, }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 1, agentMeta: { @@ -210,7 +210,7 @@ describe("updateSessionStoreAfterAgentRun", () => { }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 1, agentMeta: { @@ -262,7 +262,7 @@ describe("updateSessionStoreAfterAgentRun", () => { }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 1, executionTrace: { runner: "cli" }, @@ -313,7 +313,7 @@ describe("updateSessionStoreAfterAgentRun", () => { }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 1, agentMeta: { @@ -604,7 +604,7 @@ describe("updateSessionStoreAfterAgentRun", () => { }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 500, agentMeta: { @@ -650,7 +650,7 @@ describe("updateSessionStoreAfterAgentRun", () => { }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 500, agentMeta: { @@ -738,7 +738,7 @@ describe("updateSessionStoreAfterAgentRun", () => { }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 500, agentMeta: { @@ -901,7 +901,7 @@ describe("updateSessionStoreAfterAgentRun", () => { }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 500, agentMeta: { @@ -969,7 +969,7 @@ describe("updateSessionStoreAfterAgentRun", () => { compactionTokensAfter: 0, }, }, - } as EmbeddedPiRunResult, + } as EmbeddedAgentRunResult, }); expect(sessionStore[sessionKey]?.totalTokens).toBe(0); @@ -1054,7 +1054,7 @@ describe("updateSessionStoreAfterAgentRun", () => { // Simulate a run with 10k input + 5k output tokens // Cost = (10000 * 10 + 5000 * 30) / 1e6 = $0.25 - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 500, agentMeta: { @@ -1224,7 +1224,7 @@ describe("updateSessionStoreAfterAgentRun", () => { await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); // Heartbeat turn uses a different model - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 500, agentMeta: { @@ -1342,7 +1342,7 @@ describe("updateSessionStoreAfterAgentRun", () => { }; await fs.writeFile(storePath, JSON.stringify({ [sessionKey]: freshVisibleEntry }, null, 2)); - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 500, aborted: true, @@ -1414,7 +1414,7 @@ describe("updateSessionStoreAfterAgentRun", () => { await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); // Heartbeat turn uses a different, smaller model - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 500, agentMeta: { @@ -1459,7 +1459,7 @@ describe("updateSessionStoreAfterAgentRun", () => { }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 500, agentMeta: { @@ -1506,7 +1506,7 @@ describe("updateSessionStoreAfterAgentRun", () => { await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); // Heartbeat turn uses a different provider - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 500, agentMeta: { @@ -1556,7 +1556,7 @@ describe("updateSessionStoreAfterAgentRun", () => { }; await fs.writeFile(storePath, JSON.stringify(sessionStore, null, 2)); - const result: EmbeddedPiRunResult = { + const result: EmbeddedAgentRunResult = { meta: { durationMs: 500, agentMeta: { diff --git a/src/agents/command/session-store.ts b/src/agents/command/session-store.ts index ab302a5ec80..5169fa9cc15 100644 --- a/src/agents/command/session-store.ts +++ b/src/agents/command/session-store.ts @@ -18,7 +18,7 @@ import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js"; import { isCliProvider } from "../model-selection.js"; import { deriveSessionTotalTokens, hasNonzeroUsage } from "../usage.js"; -type RunResult = Awaited>; +type RunResult = Awaited>; const usageFormatModuleLoader = createLazyImportLoader(() => import("../../utils/usage-format.js")); const contextModuleLoader = createLazyImportLoader(() => import("../context.js")); diff --git a/src/agents/compaction-partial-summary.test.ts b/src/agents/compaction-partial-summary.test.ts index 3f73e5a8210..fa776196186 100644 --- a/src/agents/compaction-partial-summary.test.ts +++ b/src/agents/compaction-partial-summary.test.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "./runtime/index.js"; +import type { ExtensionContext } from "./sessions/index.js"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const compactionMocks = vi.hoisted(() => { @@ -25,10 +25,8 @@ const compactionMocks = vi.hoisted(() => { }; }); -vi.mock("@earendil-works/pi-coding-agent", async () => { - const actual = await vi.importActual( - "@earendil-works/pi-coding-agent", - ); +vi.mock("./sessions/index.js", async () => { + const actual = await vi.importActual("./sessions/index.js"); return { ...actual, estimateTokens: compactionMocks.estimateTokens, diff --git a/src/agents/compaction-real-conversation.ts b/src/agents/compaction-real-conversation.ts index 85280f9fe0b..fb099a0e225 100644 --- a/src/agents/compaction-real-conversation.ts +++ b/src/agents/compaction-real-conversation.ts @@ -1,6 +1,6 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { stripHeartbeatToken } from "../auto-reply/heartbeat.js"; import { isSilentReplyText } from "../auto-reply/tokens.js"; +import type { AgentMessage } from "./runtime/index.js"; const TOOL_RESULT_REAL_CONVERSATION_LOOKBACK = 20; const NON_CONVERSATION_BLOCK_TYPES = new Set([ diff --git a/src/agents/compaction.identifier-preservation.test.ts b/src/agents/compaction.identifier-preservation.test.ts index 05489d8cb50..01b08d26fc0 100644 --- a/src/agents/compaction.identifier-preservation.test.ts +++ b/src/agents/compaction.identifier-preservation.test.ts @@ -1,17 +1,17 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; -import * as piCodingAgent from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import type { ExtensionContext } from "openclaw/plugin-sdk/agent-sessions"; +import * as agentSessions from "openclaw/plugin-sdk/agent-sessions"; import { beforeEach, describe, expect, it, vi } from "vitest"; -vi.mock("@earendil-works/pi-coding-agent", async () => { - const actual = await vi.importActual("@earendil-works/pi-coding-agent"); +vi.mock("openclaw/plugin-sdk/agent-sessions", async () => { + const actual = await vi.importActual("openclaw/plugin-sdk/agent-sessions"); return { ...actual, generateSummary: vi.fn(), }; }); -const mockGenerateSummary = vi.mocked(piCodingAgent.generateSummary); +const mockGenerateSummary = vi.mocked(agentSessions.generateSummary); type SummarizeInStagesInput = Parameters[0]; const { buildCompactionSummarizationInstructions, summarizeInStages } = diff --git a/src/agents/compaction.retry.test.ts b/src/agents/compaction.retry.test.ts index a155743b331..393d0a9d1b1 100644 --- a/src/agents/compaction.retry.test.ts +++ b/src/agents/compaction.retry.test.ts @@ -1,20 +1,20 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, UserMessage } from "@earendil-works/pi-ai"; -import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; -import * as piCodingAgent from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import type { ExtensionContext } from "openclaw/plugin-sdk/agent-sessions"; +import * as agentSessions from "openclaw/plugin-sdk/agent-sessions"; +import type { AssistantMessage, UserMessage } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { retryAsync } from "../infra/retry.js"; // Mock the external generateSummary function -vi.mock("@earendil-works/pi-coding-agent", async () => { - const actual = await vi.importActual("@earendil-works/pi-coding-agent"); +vi.mock("openclaw/plugin-sdk/agent-sessions", async () => { + const actual = await vi.importActual("openclaw/plugin-sdk/agent-sessions"); return { ...actual, generateSummary: vi.fn(), }; }); -const mockGenerateSummary = vi.mocked(piCodingAgent.generateSummary); +const mockGenerateSummary = vi.mocked(agentSessions.generateSummary); type MockGenerateSummaryCompat = ( currentMessages: AgentMessage[], model: NonNullable, diff --git a/src/agents/compaction.summarize-fallback.test.ts b/src/agents/compaction.summarize-fallback.test.ts index 13bfe7d4749..360a5351799 100644 --- a/src/agents/compaction.summarize-fallback.test.ts +++ b/src/agents/compaction.summarize-fallback.test.ts @@ -1,22 +1,22 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { UserMessage } from "@earendil-works/pi-ai"; -import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import type { ExtensionContext } from "openclaw/plugin-sdk/agent-sessions"; +import type { UserMessage } from "openclaw/plugin-sdk/llm"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { summarizeWithFallback } from "./compaction.js"; -const piCodingAgentMocks = vi.hoisted(() => ({ +const agentSessionMocks = vi.hoisted(() => ({ generateSummary: vi.fn(), estimateTokens: vi.fn((_message: unknown) => 100), })); -vi.mock("@earendil-works/pi-coding-agent", async () => { - const actual = await vi.importActual( - "@earendil-works/pi-coding-agent", +vi.mock("openclaw/plugin-sdk/agent-sessions", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/agent-sessions", ); return { ...actual, - generateSummary: piCodingAgentMocks.generateSummary, - estimateTokens: piCodingAgentMocks.estimateTokens, + generateSummary: agentSessionMocks.generateSummary, + estimateTokens: agentSessionMocks.estimateTokens, }; }); @@ -30,12 +30,12 @@ const testModel = { describe("summarizeWithFallback", () => { beforeEach(() => { - piCodingAgentMocks.generateSummary.mockReset(); - piCodingAgentMocks.generateSummary.mockRejectedValue( + agentSessionMocks.generateSummary.mockReset(); + agentSessionMocks.generateSummary.mockRejectedValue( new Error("Summarization failed: fetch failed"), ); - piCodingAgentMocks.estimateTokens.mockReset(); - piCodingAgentMocks.estimateTokens.mockImplementation(() => 100); + agentSessionMocks.estimateTokens.mockReset(); + agentSessionMocks.estimateTokens.mockImplementation(() => 100); }); it("does not duplicate summarization when no messages were oversized", async () => { @@ -60,11 +60,11 @@ describe("summarizeWithFallback", () => { expect(result).toContain("Context contained 1 messages"); expect(result).toContain("0 oversized"); // "fetch failed" is timeout-classed now, so summarizeChunks does not retry it. - expect(piCodingAgentMocks.generateSummary).toHaveBeenCalledTimes(1); + expect(agentSessionMocks.generateSummary).toHaveBeenCalledTimes(1); }); it("still attempts partial summarization when oversized messages were excluded", async () => { - piCodingAgentMocks.estimateTokens.mockImplementation((message: unknown) => { + agentSessionMocks.estimateTokens.mockImplementation((message: unknown) => { const content = typeof (message as { content?: unknown }).content === "string" ? (message as { content: string }).content @@ -97,6 +97,6 @@ describe("summarizeWithFallback", () => { expect(result).toContain("2 messages (1 oversized)"); // Full attempt plus distinct partial transcript; timeout-classed failures do not retry. - expect(piCodingAgentMocks.generateSummary.mock.calls.length).toBe(2); + expect(agentSessionMocks.generateSummary.mock.calls.length).toBe(2); }); }); diff --git a/src/agents/compaction.test.ts b/src/agents/compaction.test.ts index 0d0a459451e..bda29e10775 100644 --- a/src/agents/compaction.test.ts +++ b/src/agents/compaction.test.ts @@ -1,8 +1,8 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, ToolResultMessage } from "@earendil-works/pi-ai"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import type { AssistantMessage, ToolResultMessage } from "openclaw/plugin-sdk/llm"; import { beforeAll, describe, expect, it, vi } from "vitest"; import { makeAgentAssistantMessage } from "./test-helpers/agent-message-fixtures.js"; -import "./test-helpers/pi-coding-agent-token-mock.js"; +import "./test-helpers/agent-session-token-mock.js"; let estimateMessagesTokens: typeof import("./compaction.js").estimateMessagesTokens; let pruneHistoryForContextShare: typeof import("./compaction.js").pruneHistoryForContextShare; diff --git a/src/agents/compaction.token-sanitize.test.ts b/src/agents/compaction.token-sanitize.test.ts index bc03f882975..ab6717a70db 100644 --- a/src/agents/compaction.token-sanitize.test.ts +++ b/src/agents/compaction.token-sanitize.test.ts @@ -1,19 +1,19 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it, vi } from "vitest"; -const piCodingAgentMocks = vi.hoisted(() => ({ +const agentSessionMocks = vi.hoisted(() => ({ estimateTokens: vi.fn((_message: unknown) => 1), generateSummary: vi.fn(async () => "summary"), })); -vi.mock("@earendil-works/pi-coding-agent", async () => { - const actual = await vi.importActual( - "@earendil-works/pi-coding-agent", +vi.mock("openclaw/plugin-sdk/agent-sessions", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/agent-sessions", ); return { ...actual, - estimateTokens: piCodingAgentMocks.estimateTokens, - generateSummary: piCodingAgentMocks.generateSummary, + estimateTokens: agentSessionMocks.estimateTokens, + generateSummary: agentSessionMocks.generateSummary, }; }); @@ -41,7 +41,7 @@ describe("compaction token accounting sanitization", () => { splitMessagesByTokenShare(messages, 2); chunkMessagesByMaxTokens(messages, 16); - const calledWithDetails = piCodingAgentMocks.estimateTokens.mock.calls.some((call) => { + const calledWithDetails = agentSessionMocks.estimateTokens.mock.calls.some((call) => { const message = call[0] as { details?: unknown } | undefined; return Boolean(message?.details); }); diff --git a/src/agents/compaction.tool-result-details.test.ts b/src/agents/compaction.tool-result-details.test.ts index 9249b5a9c52..15b3a7c52b3 100644 --- a/src/agents/compaction.tool-result-details.test.ts +++ b/src/agents/compaction.tool-result-details.test.ts @@ -1,21 +1,21 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, ToolResultMessage } from "@earendil-works/pi-ai"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import type { AssistantMessage, ToolResultMessage } from "openclaw/plugin-sdk/llm"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { makeAgentAssistantMessage } from "./test-helpers/agent-message-fixtures.js"; -const piCodingAgentMocks = vi.hoisted(() => ({ +const agentSessionMocks = vi.hoisted(() => ({ generateSummary: vi.fn(async () => "summary"), estimateTokens: vi.fn((_message: unknown) => 1), })); -vi.mock("@earendil-works/pi-coding-agent", async () => { - const actual = await vi.importActual( - "@earendil-works/pi-coding-agent", +vi.mock("openclaw/plugin-sdk/agent-sessions", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/agent-sessions", ); return { ...actual, - generateSummary: piCodingAgentMocks.generateSummary, - estimateTokens: piCodingAgentMocks.estimateTokens, + generateSummary: agentSessionMocks.generateSummary, + estimateTokens: agentSessionMocks.estimateTokens, }; }); @@ -51,10 +51,10 @@ function makeToolResultWithDetails(timestamp: number): ToolResultMessage<{ raw: describe("compaction toolResult details stripping", () => { beforeEach(async () => { await loadFreshCompactionModuleForTest(); - piCodingAgentMocks.generateSummary.mockReset(); - piCodingAgentMocks.generateSummary.mockResolvedValue("summary"); - piCodingAgentMocks.estimateTokens.mockReset(); - piCodingAgentMocks.estimateTokens.mockImplementation((_message: unknown) => 1); + agentSessionMocks.generateSummary.mockReset(); + agentSessionMocks.generateSummary.mockResolvedValue("summary"); + agentSessionMocks.estimateTokens.mockReset(); + agentSessionMocks.estimateTokens.mockImplementation((_message: unknown) => 1); }); it("does not pass toolResult.details into generateSummary", async () => { @@ -72,10 +72,10 @@ describe("compaction toolResult details stripping", () => { }); expect(summary).toBe("summary"); - expect(piCodingAgentMocks.generateSummary).toHaveBeenCalledTimes(1); + expect(agentSessionMocks.generateSummary).toHaveBeenCalledTimes(1); const chunk = ( - piCodingAgentMocks.generateSummary.mock.calls as unknown as Array<[AgentMessage[]]> + agentSessionMocks.generateSummary.mock.calls as unknown as Array<[AgentMessage[]]> )[0]?.[0]; expect(chunk).toStrictEqual([ { @@ -141,9 +141,9 @@ describe("compaction toolResult details stripping", () => { contextWindow: 10000, }); - expect(piCodingAgentMocks.generateSummary).toHaveBeenCalledTimes(1); + expect(agentSessionMocks.generateSummary).toHaveBeenCalledTimes(1); const chunk = ( - piCodingAgentMocks.generateSummary.mock.calls as unknown as Array<[AgentMessage[]]> + agentSessionMocks.generateSummary.mock.calls as unknown as Array<[AgentMessage[]]> )[0]?.[0]; expect(chunk).toStrictEqual([ { role: "user", content: "visible ask", timestamp: 1 }, @@ -156,7 +156,7 @@ describe("compaction toolResult details stripping", () => { }); it("ignores toolResult.details when evaluating oversized messages", () => { - piCodingAgentMocks.estimateTokens.mockImplementation((message: unknown) => { + agentSessionMocks.estimateTokens.mockImplementation((message: unknown) => { const record = message as { details?: unknown }; return record.details ? 10_000 : 10; }); diff --git a/src/agents/compaction.ts b/src/agents/compaction.ts index 1f3ce22d8b7..a66e4adfaa6 100644 --- a/src/agents/compaction.ts +++ b/src/agents/compaction.ts @@ -1,9 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; -import { - estimateTokens, - generateSummary as piGenerateSummary, -} from "@earendil-works/pi-coding-agent"; import type { AgentCompactionIdentifierPolicy } from "../config/types.agent-defaults.js"; import { formatErrorMessage } from "../infra/errors.js"; import { retryAsync } from "../infra/retry.js"; @@ -14,7 +8,10 @@ import { isTimeoutError } from "./failover-error.js"; type PartialSummaryError = Error & { partialSummary?: string }; import { stripRuntimeContextCustomMessages } from "./internal-runtime-context.js"; +import type { AgentMessage } from "./runtime/index.js"; import { repairToolUseResultPairing, stripToolResultDetails } from "./session-transcript-repair.js"; +import type { ExtensionContext } from "./sessions/index.js"; +import { estimateTokens, generateSummary as agentGenerateSummary } from "./sessions/index.js"; import { extractToolCallsFromAssistant, extractToolResultId } from "./tool-call-id.js"; const log = createSubsystemLogger("compaction"); @@ -85,7 +82,7 @@ type GenerateSummaryCompat = { ): Promise; }; -const generateSummaryCompat = piGenerateSummary as unknown as GenerateSummaryCompat; +const generateSummaryCompat = agentGenerateSummary as unknown as GenerateSummaryCompat; function resolveIdentifierPreservationInstructions( instructions?: CompactionSummarizationInstructions, @@ -402,7 +399,7 @@ function generateSummary( customInstructions?: string, previousSummary?: string, ): Promise { - if (piGenerateSummary.length >= 8) { + if (agentGenerateSummary.length >= 8) { return generateSummaryCompat( currentMessages, model, diff --git a/src/agents/config.ts b/src/agents/config.ts new file mode 100644 index 00000000000..2b533133cbb --- /dev/null +++ b/src/agents/config.ts @@ -0,0 +1,564 @@ +import { accessSync, constants, existsSync, readFileSync, realpathSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, dirname, join, resolve, sep, win32 } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnProcessSync } from "./utils/child-process.ts"; + +// ============================================================================= +// Package Detection +// ============================================================================= + +const currentFile = fileURLToPath(import.meta.url); +const currentDir = dirname(currentFile); + +/** + * Detect if we're running as a Bun compiled binary. + * Bun binaries have import.meta.url containing "$bunfs", "~BUN", or "%7EBUN" (Bun's virtual filesystem path) + */ +export const isBunBinary = + import.meta.url.includes("$bunfs") || + import.meta.url.includes("~BUN") || + import.meta.url.includes("%7EBUN"); + +/** Detect if Bun is the runtime (compiled binary or bun run) */ +export const isBunRuntime = !!process.versions.bun; + +// ============================================================================= +// Install Method Detection +// ============================================================================= + +export type InstallMethod = "bun-binary" | "npm" | "pnpm" | "yarn" | "bun" | "unknown"; + +interface SelfUpdateCommandStep { + command: string; + args: string[]; + display: string; +} + +export interface SelfUpdateCommand extends SelfUpdateCommandStep { + steps?: SelfUpdateCommandStep[]; +} + +function makeSelfUpdateCommand( + installStep: SelfUpdateCommandStep, + uninstallStep?: SelfUpdateCommandStep, +): SelfUpdateCommand { + if (!uninstallStep) { + return installStep; + } + return { + ...installStep, + display: `${uninstallStep.display} && ${installStep.display}`, + steps: [uninstallStep, installStep], + }; +} + +function makeSelfUpdateCommandStep(command: string, args: string[]): SelfUpdateCommandStep { + return { + command, + args, + display: [command, ...args].map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" "), + }; +} + +export function detectInstallMethod(): InstallMethod { + if (isBunBinary) { + return "bun-binary"; + } + + const resolvedPath = `${currentDir}\0${process.execPath || ""}`.toLowerCase().replace(/\\/g, "/"); + + if (resolvedPath.includes("/pnpm/") || resolvedPath.includes("/.pnpm/")) { + return "pnpm"; + } + if (resolvedPath.includes("/yarn/") || resolvedPath.includes("/.yarn/")) { + return "yarn"; + } + if (isBunRuntime || resolvedPath.includes("/install/global/node_modules/")) { + return "bun"; + } + if (resolvedPath.includes("/npm/") || resolvedPath.includes("/node_modules/")) { + return "npm"; + } + + return "unknown"; +} + +function getInferredNpmInstall(): { root: string; prefix: string } | undefined { + const packageDir = getPackageDir(); + const path = + process.platform === "win32" || packageDir.includes("\\") ? win32 : { basename, dirname }; + const parent = path.dirname(packageDir); + let root: string | undefined; + if ( + path.basename(parent).startsWith("@") && + path.basename(path.dirname(parent)) === "node_modules" + ) { + root = path.dirname(parent); + } else if (path.basename(parent) === "node_modules") { + root = parent; + } + if (!root) { + return undefined; + } + const rootParent = path.dirname(root); + if (path.basename(rootParent) === "lib") { + return { root, prefix: path.dirname(rootParent) }; + } + // Windows global npm prefixes use `\\node_modules`, which is + // indistinguishable from local project installs by path shape alone. Do not + // infer unsupported Windows custom prefixes without `npm root -g` evidence. + return undefined; +} + +function getSelfUpdateCommandForMethod( + method: InstallMethod, + installedPackageName: string, + updatePackageName = installedPackageName, + npmCommand?: string[], +): SelfUpdateCommand | undefined { + switch (method) { + case "bun-binary": + return undefined; + case "pnpm": + return makeSelfUpdateCommand( + makeSelfUpdateCommandStep("pnpm", ["install", "-g", "--ignore-scripts", updatePackageName]), + updatePackageName === installedPackageName + ? undefined + : makeSelfUpdateCommandStep("pnpm", ["remove", "-g", installedPackageName]), + ); + case "yarn": + return makeSelfUpdateCommand( + makeSelfUpdateCommandStep("yarn", ["global", "add", "--ignore-scripts", updatePackageName]), + updatePackageName === installedPackageName + ? undefined + : makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]), + ); + case "bun": + return makeSelfUpdateCommand( + makeSelfUpdateCommandStep("bun", ["install", "-g", "--ignore-scripts", updatePackageName]), + updatePackageName === installedPackageName + ? undefined + : makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]), + ); + case "npm": { + const [command = "npm", ...npmArgs] = npmCommand ?? []; + const inferred = npmCommand?.length ? undefined : getInferredNpmInstall(); + const prefixArgs = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : [])]; + const installStep = makeSelfUpdateCommandStep(command, [ + ...prefixArgs, + "install", + "-g", + "--ignore-scripts", + updatePackageName, + ]); + const uninstallStep = + updatePackageName === installedPackageName + ? undefined + : makeSelfUpdateCommandStep(command, [ + ...prefixArgs, + "uninstall", + "-g", + installedPackageName, + ]); + return makeSelfUpdateCommand(installStep, uninstallStep); + } + case "unknown": + return undefined; + } + return undefined; +} + +function readCommandOutput( + command: string, + args: string[], + options: { requireSuccess?: boolean } = {}, +): string | undefined { + const result = spawnProcessSync(command, args, { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }); + if (result.status === 0) { + return result.stdout.trim() || undefined; + } + if (options.requireSuccess) { + const reason = + result.error?.message || result.stderr.trim() || `exit code ${result.status ?? "unknown"}`; + throw new Error(`Failed to run ${[command, ...args].join(" ")}: ${reason}`); + } + return undefined; +} + +function getGlobalPackageRoots( + method: InstallMethod, + _packageName: string, + npmCommand?: string[], +): string[] { + switch (method) { + case "npm": { + const configured = !!npmCommand?.length; + const [command = "npm", ...npmArgs] = npmCommand ?? []; + if (configured && command === "bun") { + const bunBin = readCommandOutput(command, [...npmArgs, "pm", "bin", "-g"], { + requireSuccess: true, + }); + const roots = [join(homedir(), ".bun", "install", "global", "node_modules")]; + if (bunBin) { + roots.push(join(dirname(bunBin), "install", "global", "node_modules")); + } + return roots; + } + const root = readCommandOutput(command, [...npmArgs, "root", "-g"], { + requireSuccess: configured, + }); + const inferred = configured ? undefined : getInferredNpmInstall(); + return [root, inferred?.root].filter((x): x is string => !!x); + } + case "pnpm": { + const root = readCommandOutput("pnpm", ["root", "-g"]); + return root ? [root, dirname(root)] : []; + } + case "yarn": { + const dir = readCommandOutput("yarn", ["global", "dir"]); + return dir ? [dir, join(dir, "node_modules")] : []; + } + case "bun": { + const bunBin = readCommandOutput("bun", ["pm", "bin", "-g"]); + const roots = [join(homedir(), ".bun", "install", "global", "node_modules")]; + if (bunBin) { + roots.push(join(dirname(bunBin), "install", "global", "node_modules")); + } + return roots; + } + case "bun-binary": + case "unknown": + return []; + } + return []; +} + +function normalizeExistingPathForComparison( + path: string, + resolveSymlinks: boolean, +): string | undefined { + const resolvedPath = resolve(path); + if (!existsSync(resolvedPath)) { + return undefined; + } + let normalizedPath = resolvedPath; + if (resolveSymlinks) { + try { + normalizedPath = realpathSync(resolvedPath); + } catch { + return undefined; + } + } + if (process.platform === "win32") { + normalizedPath = normalizedPath.toLowerCase(); + } + return normalizedPath; +} + +function getPathComparisonCandidates(path: string): string[] { + return Array.from( + new Set( + [ + normalizeExistingPathForComparison(path, false), + normalizeExistingPathForComparison(path, true), + ].filter((candidate): candidate is string => !!candidate), + ), + ); +} + +function getEntrypointPackageDir(): string | undefined { + const entrypoint = process.argv[1]; + if (!entrypoint) { + return undefined; + } + let dir = dirname(entrypoint); + while (dir !== dirname(dir)) { + if (existsSync(join(dir, "package.json"))) { + return dir; + } + dir = dirname(dir); + } + return undefined; +} + +function isSelfUpdatePathWritable(): boolean { + const packageDir = getPackageDir(); + try { + accessSync(packageDir, constants.W_OK); + accessSync(dirname(packageDir), constants.W_OK); + return true; + } catch { + return false; + } +} + +function isManagedByGlobalPackageManager( + method: InstallMethod, + packageName: string, + npmCommand?: string[], +): boolean { + const packageDirs = [getPackageDir(), getEntrypointPackageDir()].filter( + (dir): dir is string => !!dir, + ); + const packageDirCandidates = packageDirs.flatMap((dir) => getPathComparisonCandidates(dir)); + return getGlobalPackageRoots(method, packageName, npmCommand).some((root) => { + return getPathComparisonCandidates(root).some((normalizedRoot) => { + const rootPrefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`; + return packageDirCandidates.some((packageDir) => packageDir.startsWith(rootPrefix)); + }); + }); +} + +export function getSelfUpdateCommand( + packageName: string, + npmCommand?: string[], + updatePackageName = packageName, +): SelfUpdateCommand | undefined { + const method = detectInstallMethod(); + const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand); + if ( + !command || + !isManagedByGlobalPackageManager(method, packageName, npmCommand) || + !isSelfUpdatePathWritable() + ) { + return undefined; + } + return command; +} + +export function getSelfUpdateUnavailableInstruction( + packageName: string, + npmCommand?: string[], + updatePackageName = packageName, +): string { + const method = detectInstallMethod(); + if (method === "bun-binary") { + return `Download from: https://github.com/openclaw/openclaw/releases/latest`; + } + const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand); + if (command) { + if ( + isManagedByGlobalPackageManager(method, packageName, npmCommand) && + !isSelfUpdatePathWritable() + ) { + return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`; + } + return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`; + } + return `Update ${updatePackageName} using the package manager, wrapper, or source checkout that provides this installation.`; +} + +export function getUpdateInstruction(packageName: string): string { + const method = detectInstallMethod(); + const command = getSelfUpdateCommandForMethod(method, packageName); + if (command) { + return `Run: ${command.display}`; + } + return getSelfUpdateUnavailableInstruction(packageName); +} + +// ============================================================================= +// Package Asset Paths (shipped with executable) +// ============================================================================= + +/** + * Get the base directory for resolving package assets (themes, package.json, README.md, CHANGELOG.md). + * - For Bun binary: returns the directory containing the executable + * - For Node.js (dist/): returns currentDir (the dist/ directory) + * - For tsx (src/): returns parent directory (the package root) + */ +export function getPackageDir(): string { + // Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly) + const envDir = process.env.OPENCLAW_PACKAGE_DIR; + if (envDir) { + if (envDir === "~") { + return homedir(); + } + if (envDir.startsWith("~/")) { + return homedir() + envDir.slice(1); + } + return envDir; + } + + if (isBunBinary) { + // Bun binary: process.execPath points to the compiled executable + return dirname(process.execPath); + } + // Node.js: walk up from currentDir until we find package.json + let dir = currentDir; + while (dir !== dirname(dir)) { + if (existsSync(join(dir, "package.json"))) { + return dir; + } + dir = dirname(dir); + } + // Fallback (shouldn't happen) + return currentDir; +} + +function getPackageSourceOrDistDir(): string { + const packageDir = getPackageDir(); + const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist"; + return join(packageDir, srcOrDist); +} + +/** + * Get path to built-in themes directory (shipped with package) + * - For Bun binary: theme/ next to executable + * - For Node.js (dist/): dist/agents/modes/interactive/theme/ + * - For tsx (src/): src/agents/modes/interactive/theme/ + */ +export function getThemesDir(): string { + if (isBunBinary) { + return join(getPackageDir(), "theme"); + } + return join(getPackageSourceOrDistDir(), "agents", "modes", "interactive", "theme"); +} + +/** Get path to package.json */ +export function getPackageJsonPath(): string { + return join(getPackageDir(), "package.json"); +} + +/** Get path to README.md */ +export function getReadmePath(): string { + return resolve(join(getPackageDir(), "README.md")); +} + +/** Get path to docs directory */ +export function getDocsPath(): string { + return resolve(join(getPackageDir(), "docs")); +} + +/** Get path to examples directory */ +export function getExamplesPath(): string { + return resolve(join(getPackageDir(), "examples")); +} + +/** Get path to CHANGELOG.md */ +export function getChangelogPath(): string { + return resolve(join(getPackageDir(), "CHANGELOG.md")); +} + +/** + * Get path to built-in interactive assets directory. + * - For Bun binary: assets/ next to executable + * - For Node.js (dist/): dist/agents/modes/interactive/assets/ + * - For tsx (src/): src/agents/modes/interactive/assets/ + */ +export function getInteractiveAssetsDir(): string { + if (isBunBinary) { + return join(getPackageDir(), "assets"); + } + return join(getPackageSourceOrDistDir(), "agents", "modes", "interactive", "assets"); +} + +/** Get path to a bundled interactive asset */ +export function getBundledInteractiveAssetPath(name: string): string { + return join(getInteractiveAssetsDir(), name); +} + +// ============================================================================= +// App Config (from package.json openclawConfig) +// ============================================================================= + +interface PackageJson { + name?: string; + version?: string; + openclawConfig?: { + name?: string; + configDir?: string; + }; +} + +const pkg = JSON.parse(readFileSync(getPackageJsonPath(), "utf-8")) as PackageJson; + +const openClawConfigName: string | undefined = pkg.openclawConfig?.name; +export const PACKAGE_NAME: string = pkg.name || "openclaw/plugin-sdk/agent-sessions"; +export const APP_NAME: string = openClawConfigName || "openclaw"; +export const APP_TITLE: string = openClawConfigName ? APP_NAME : "OpenClaw"; +export const CONFIG_DIR_NAME: string = pkg.openclawConfig?.configDir || ".openclaw"; +export const VERSION: string = pkg.version || "0.0.0"; + +export const ENV_AGENT_DIR = `${APP_NAME.toUpperCase()}_AGENT_DIR`; +export const ENV_SESSION_DIR = `${APP_NAME.toUpperCase()}_AGENT_SESSION_DIR`; + +export function expandTildePath(path: string): string { + if (path === "~") { + return homedir(); + } + if (path.startsWith("~/")) { + return homedir() + path.slice(1); + } + return path; +} + +const DEFAULT_SHARE_VIEWER_URL = "https://openclaw.ai/session/"; + +/** Get the share viewer URL for a gist ID */ +export function getShareViewerUrl(gistId: string): string { + const baseUrl = process.env.OPENCLAW_SHARE_VIEWER_URL || DEFAULT_SHARE_VIEWER_URL; + return `${baseUrl}#${gistId}`; +} + +// ============================================================================= +// User Config Paths (~/.openclaw/agent/*) +// ============================================================================= + +/** Get the agent config directory (e.g., ~/.openclaw/agent/) */ +export function getAgentDir(): string { + const envDir = process.env[ENV_AGENT_DIR]; + if (envDir) { + return expandTildePath(envDir); + } + return join(homedir(), CONFIG_DIR_NAME, "agent"); +} + +/** Get path to user's custom themes directory */ +export function getCustomThemesDir(): string { + return join(getAgentDir(), "themes"); +} + +/** Get path to models.json */ +export function getModelsPath(): string { + return join(getAgentDir(), "models.json"); +} + +/** Get path to auth.json */ +export function getAuthPath(): string { + return join(getAgentDir(), "auth.json"); +} + +/** Get path to settings.json */ +export function getSettingsPath(): string { + return join(getAgentDir(), "settings.json"); +} + +/** Get path to tools directory */ +export function getToolsDir(): string { + return join(getAgentDir(), "tools"); +} + +/** Get path to managed binaries directory (fd, rg) */ +export function getBinDir(): string { + return join(getAgentDir(), "bin"); +} + +/** Get path to prompt templates directory */ +export function getPromptsDir(): string { + return join(getAgentDir(), "prompts"); +} + +/** Get path to sessions directory */ +export function getSessionsDir(): string { + return join(getAgentDir(), "sessions"); +} + +/** Get path to debug log file */ +export function getDebugLogPath(): string { + return join(getAgentDir(), `${APP_NAME}-debug.log`); +} diff --git a/src/agents/context.lookup.test.ts b/src/agents/context.lookup.test.ts index 4885a3f861f..c4dee9fbe69 100644 --- a/src/agents/context.lookup.test.ts +++ b/src/agents/context.lookup.test.ts @@ -32,7 +32,7 @@ vi.mock("./models-config.runtime.js", () => ({ ensureOpenClawModelsJson: contextTestState.ensureOpenClawModelsJson, })); -vi.mock("./pi-model-discovery-runtime.js", () => ({ +vi.mock("./agent-model-discovery.js", () => ({ discoverAuthStorage: contextTestState.discoverAuthStorage, discoverModels: contextTestState.discoverModels, })); @@ -380,7 +380,7 @@ describe("lookupContextTokens", () => { it("resolveContextTokensForModel prefers exact provider key over alias-normalized match", async () => { // When both "bedrock" and "amazon-bedrock" exist as config keys (alias pattern), // resolveConfiguredProviderContextWindow must return the exact-key match first, - // not the first normalized hit — mirroring pi-embedded-runner/model.ts behaviour. + // not the first normalized hit — mirroring embedded-agent-runner/model.ts behaviour. mockDiscoveryDeps([]); const cfg = { diff --git a/src/agents/context.test.ts b/src/agents/context.test.ts index 01703a6669f..b1c9aaf479e 100644 --- a/src/agents/context.test.ts +++ b/src/agents/context.test.ts @@ -1,11 +1,11 @@ import { describe, expect, it, vi } from "vitest"; +import { createSessionManagerRuntimeRegistry } from "./agent-hooks/session-manager-runtime-registry.js"; import { ANTHROPIC_CONTEXT_1M_TOKENS, applyConfiguredContextWindows, applyDiscoveredContextWindows, resolveContextTokensForModel, } from "./context.js"; -import { createSessionManagerRuntimeRegistry } from "./pi-hooks/session-manager-runtime-registry.js"; vi.mock("../config/config.js", () => ({ getRuntimeConfig: () => ({}) })); diff --git a/src/agents/context.ts b/src/agents/context.ts index 277aec39713..7a82cae3d36 100644 --- a/src/agents/context.ts +++ b/src/agents/context.ts @@ -1,10 +1,11 @@ -// Lazy-load pi-coding-agent model metadata so we can infer context windows when -// the agent reports a model id. This includes custom models.json entries. +// Load session runtime model metadata so we can infer context windows when the +// agent reports a model id. This includes custom models.json entries. import { getRuntimeConfig } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { computeBackoff, type BackoffPolicy } from "../infra/backoff.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { discoverAuthStorage, discoverModels } from "./agent-model-discovery.js"; import { resolveDefaultAgentDir } from "./agent-scope.js"; import { lookupCachedContextTokens, MODEL_CONTEXT_TOKEN_CACHE } from "./context-cache.js"; import { CONTEXT_WINDOW_RUNTIME_STATE } from "./context-runtime-state.js"; @@ -164,8 +165,6 @@ export function ensureContextWindowCacheLoaded(): Promise { } try { - const { discoverAuthStorage, discoverModels } = - await import("./pi-model-discovery-runtime.js"); const agentDir = resolveDefaultAgentDir(cfg); const authStorage = discoverAuthStorage(agentDir); const modelRegistry = discoverModels(authStorage, agentDir, { @@ -257,7 +256,7 @@ function resolveConfiguredProviderContextTokens( return undefined; } - // Mirror the lookup order in pi-embedded-runner/model.ts: exact key first, + // Mirror the lookup order in embedded-agent-runner/model.ts: exact key first, // then normalized fallback. This prevents alias collisions from picking the // wrong configured cap based on Object.entries iteration order. function readProviderContextTokens(providerConfig: ProviderConfigEntry | undefined) { diff --git a/src/agents/copilot-dynamic-headers.ts b/src/agents/copilot-dynamic-headers.ts index 879ab9e3ebf..8743c25c706 100644 --- a/src/agents/copilot-dynamic-headers.ts +++ b/src/agents/copilot-dynamic-headers.ts @@ -1,4 +1,4 @@ -import type { Context } from "@earendil-works/pi-ai"; +import type { Context } from "openclaw/plugin-sdk/llm"; /** @deprecated GitHub Copilot provider-owned helper; do not use from third-party plugins. */ export const COPILOT_EDITOR_VERSION = "vscode/1.107.0"; diff --git a/src/agents/custom-api-registry.test.ts b/src/agents/custom-api-registry.test.ts index 3b222b2a0e2..d9487a9e0d3 100644 --- a/src/agents/custom-api-registry.test.ts +++ b/src/agents/custom-api-registry.test.ts @@ -3,8 +3,10 @@ import { createAssistantMessageEventStream, getApiProvider, registerBuiltInApiProviders, + registerApiProvider, + resetApiProviders, unregisterApiProviders, -} from "@earendil-works/pi-ai"; +} from "openclaw/plugin-sdk/llm"; import { afterEach, describe, expect, it, vi } from "vitest"; import { ensureCustomApiRegistered, getCustomApiRegistrySourceId } from "./custom-api-registry.js"; @@ -49,4 +51,26 @@ describe("ensureCustomApiRegistered", () => { expect(provider.streamSimple(model as never, context as never, options as never)).toBe(stream); expect(streamFn).toHaveBeenCalledTimes(2); }); + + it("keeps plugin api providers when refreshing built-ins", () => { + const sourceId = "plugin:test-reset-api"; + const api = "test-reset-plugin-api"; + const streamFn = vi.fn(() => createAssistantMessageEventStream()); + const streamSimpleFn = vi.fn(() => createAssistantMessageEventStream()); + registerApiProvider( + { + api, + stream: streamFn, + streamSimple: streamSimpleFn, + }, + sourceId, + ); + + resetApiProviders(); + + expect(getApiProvider(api)).toBeDefined(); + expect(getApiProvider("openai-responses")).toBeDefined(); + + unregisterApiProviders(sourceId); + }); }); diff --git a/src/agents/custom-api-registry.ts b/src/agents/custom-api-registry.ts index 51d687a4dd7..ad73b7197b3 100644 --- a/src/agents/custom-api-registry.ts +++ b/src/agents/custom-api-registry.ts @@ -1,10 +1,10 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; import { getApiProvider, registerApiProvider, type Api, type StreamOptions, -} from "@earendil-works/pi-ai"; +} from "openclaw/plugin-sdk/llm"; +import type { StreamFn } from "./runtime/index.js"; const CUSTOM_API_SOURCE_PREFIX = "openclaw-custom-api:"; diff --git a/src/agents/pi-embedded-block-chunker.test.ts b/src/agents/embedded-agent-block-chunker.test.ts similarity index 98% rename from src/agents/pi-embedded-block-chunker.test.ts rename to src/agents/embedded-agent-block-chunker.test.ts index f7efab3c8a9..934be79887c 100644 --- a/src/agents/pi-embedded-block-chunker.test.ts +++ b/src/agents/embedded-agent-block-chunker.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import * as fences from "../markdown/fences.js"; -import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js"; +import { EmbeddedBlockChunker } from "./embedded-agent-block-chunker.js"; function createFlushOnParagraphChunker(params: { minChars: number; maxChars: number }) { return new EmbeddedBlockChunker({ diff --git a/src/agents/pi-embedded-block-chunker.ts b/src/agents/embedded-agent-block-chunker.ts similarity index 100% rename from src/agents/pi-embedded-block-chunker.ts rename to src/agents/embedded-agent-block-chunker.ts diff --git a/src/agents/pi-embedded-error-observation.test.ts b/src/agents/embedded-agent-error-observation.test.ts similarity index 99% rename from src/agents/pi-embedded-error-observation.test.ts rename to src/agents/embedded-agent-error-observation.test.ts index c5372883adc..5c29e67fd78 100644 --- a/src/agents/pi-embedded-error-observation.test.ts +++ b/src/agents/embedded-agent-error-observation.test.ts @@ -5,7 +5,7 @@ import { buildTextObservationFields, sanitizeForConsole, shouldSuppressRawErrorConsoleSuffix, -} from "./pi-embedded-error-observation.js"; +} from "./embedded-agent-error-observation.js"; const OBSERVATION_BEARER_TOKEN = "sk-redact-test-token"; const OBSERVATION_COOKIE_VALUE = "session-cookie-token"; diff --git a/src/agents/pi-embedded-error-observation.ts b/src/agents/embedded-agent-error-observation.ts similarity index 99% rename from src/agents/pi-embedded-error-observation.ts rename to src/agents/embedded-agent-error-observation.ts index 6275c107353..f8948318cbf 100644 --- a/src/agents/pi-embedded-error-observation.ts +++ b/src/agents/embedded-agent-error-observation.ts @@ -7,7 +7,7 @@ import { getApiErrorPayloadFingerprint, parseApiErrorInfo, type ProviderRuntimeFailureKind, -} from "./pi-embedded-helpers.js"; +} from "./embedded-agent-helpers.js"; import { stableStringify } from "./stable-stringify.js"; export { sanitizeForConsole } from "./console-sanitize.js"; diff --git a/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.test.ts b/src/agents/embedded-agent-helpers.buildbootstrapcontextfiles.test.ts similarity index 99% rename from src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.test.ts rename to src/agents/embedded-agent-helpers.buildbootstrapcontextfiles.test.ts index 0042ffbee40..d78e35970aa 100644 --- a/src/agents/pi-embedded-helpers.buildbootstrapcontextfiles.test.ts +++ b/src/agents/embedded-agent-helpers.buildbootstrapcontextfiles.test.ts @@ -12,7 +12,7 @@ import { resolveBootstrapMaxChars, resolveBootstrapPromptTruncationWarningMode, resolveBootstrapTotalMaxChars, -} from "./pi-embedded-helpers.js"; +} from "./embedded-agent-helpers.js"; import type { WorkspaceBootstrapFile } from "./workspace.js"; import { DEFAULT_AGENTS_FILENAME } from "./workspace.js"; diff --git a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts b/src/agents/embedded-agent-helpers.formatassistanterrortext.test.ts similarity index 99% rename from src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts rename to src/agents/embedded-agent-helpers.formatassistanterrortext.test.ts index 3c1c8c194fa..9fbc40d7d6e 100644 --- a/src/agents/pi-embedded-helpers.formatassistanterrortext.test.ts +++ b/src/agents/embedded-agent-helpers.formatassistanterrortext.test.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE } from "../shared/assistant-error-format.js"; import { @@ -9,7 +9,7 @@ import { formatRawAssistantErrorForUi, isRawApiErrorPayload, sanitizeUserFacingText, -} from "./pi-embedded-helpers.js"; +} from "./embedded-agent-helpers.js"; import { makeAssistantMessageFixture } from "./test-helpers/assistant-message-fixtures.js"; describe("formatAssistantErrorText", () => { @@ -415,7 +415,7 @@ describe("formatAssistantErrorText", () => { ); }); - it("returns an HTML-403 auth message for HTML provider auth failures", () => { + it("returns re-authentication copy for HTML provider 403 auth failures", () => { const msg = makeAssistantError("403 Access denied"); expect(formatAssistantErrorText(msg)).toBe( "Authentication failed at the provider. Re-authenticate and verify your provider credentials and account access.", diff --git a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts b/src/agents/embedded-agent-helpers.isbillingerrormessage.test.ts similarity index 99% rename from src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts rename to src/agents/embedded-agent-helpers.isbillingerrormessage.test.ts index 61a602cdfeb..114ebf222f9 100644 --- a/src/agents/pi-embedded-helpers.isbillingerrormessage.test.ts +++ b/src/agents/embedded-agent-helpers.isbillingerrormessage.test.ts @@ -18,7 +18,7 @@ import { isTransientHttpError, parseImageDimensionError, parseImageSizeError, -} from "./pi-embedded-helpers.js"; +} from "./embedded-agent-helpers.js"; // OpenAI 429 example shape: https://help.openai.com/en/articles/5955604-how-can-i-solve-429-too-many-requests-errors const OPENAI_RATE_LIMIT_MESSAGE = @@ -398,10 +398,10 @@ describe("isContextOverflowError", () => { } }); - it("matches model_context_window_exceeded stop reason surfaced by pi-ai", () => { + it("matches model_context_window_exceeded stop reason surfaced by shared model runtime", () => { // Anthropic API (and some OpenAI-compatible providers like ZhipuAI/GLM) return // stop_reason: "model_context_window_exceeded" when the context window is hit. - // The pi-ai library surfaces this as "Unhandled stop reason: model_context_window_exceeded". + // The shared model runtime library surfaces this as "Unhandled stop reason: model_context_window_exceeded". const samples = [ "Unhandled stop reason: model_context_window_exceeded", "model_context_window_exceeded", @@ -820,8 +820,8 @@ describe("classifyFailoverReason HTTP 410 handling", () => { expect(isFailoverErrorMessage(message)).toBe(true); }); - it("classifies bare pi-ai stream wrapper as timeout regardless of provider (#71620)", () => { - // pi-ai providers throw `Error("An unknown error occurred")` provider-agnostically + it("classifies bare shared model runtime stream wrapper as timeout regardless of provider (#71620)", () => { + // shared model runtime providers throw `Error("An unknown error occurred")` provider-agnostically // when streams end with stopReason "aborted" | "error" with no specific info. for (const sample of [ "An unknown error occurred", @@ -990,7 +990,7 @@ describe("isFailoverErrorMessage", () => { ]); }); - it("matches pi-ai openai-codex bare transport failures as timeout (#69368)", () => { + it("matches shared model runtime openai-codex bare transport failures as timeout (#69368)", () => { expectTimeoutFailoverSamples([ "Request failed", "request failed", diff --git a/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts b/src/agents/embedded-agent-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts similarity index 99% rename from src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts rename to src/agents/embedded-agent-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts index 4ddc09fc69e..e50e7775029 100644 --- a/src/agents/pi-embedded-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts +++ b/src/agents/embedded-agent-helpers.sanitize-session-messages-images.removes-empty-assistant-text-blocks-but-preserves.test.ts @@ -1,10 +1,10 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, ToolResultMessage, UserMessage } from "@earendil-works/pi-ai"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import type { AssistantMessage, ToolResultMessage, UserMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { sanitizeGoogleTurnOrdering, sanitizeSessionMessagesImages, -} from "./pi-embedded-helpers.js"; +} from "./embedded-agent-helpers.js"; import { castAgentMessages, makeAgentAssistantMessage, diff --git a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts b/src/agents/embedded-agent-helpers.sanitizeuserfacingtext.test.ts similarity index 99% rename from src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts rename to src/agents/embedded-agent-helpers.sanitizeuserfacingtext.test.ts index ecc8eb66373..79aaf367093 100644 --- a/src/agents/pi-embedded-helpers.sanitizeuserfacingtext.test.ts +++ b/src/agents/embedded-agent-helpers.sanitizeuserfacingtext.test.ts @@ -1,9 +1,4 @@ import { describe, expect, it } from "vitest"; -import { - formatAgentInternalEventsForPrompt, - INTERNAL_RUNTIME_CONTEXT_BEGIN, - INTERNAL_RUNTIME_CONTEXT_END, -} from "./internal-events.js"; import { downgradeOpenAIFunctionCallReasoningPairs, downgradeOpenAIReasoningBlocks, @@ -12,7 +7,12 @@ import { sanitizeToolCallId, sanitizeUserFacingText, stripThoughtSignatures, -} from "./pi-embedded-helpers.js"; +} from "./embedded-agent-helpers.js"; +import { + formatAgentInternalEventsForPrompt, + INTERNAL_RUNTIME_CONTEXT_BEGIN, + INTERNAL_RUNTIME_CONTEXT_END, +} from "./internal-events.js"; describe("sanitizeUserFacingText", () => { it("strips final tags", () => { diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/embedded-agent-helpers.ts similarity index 73% rename from src/agents/pi-embedded-helpers.ts rename to src/agents/embedded-agent-helpers.ts index ce47ef8dcd0..e1b0fdd5c79 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/embedded-agent-helpers.ts @@ -8,7 +8,7 @@ export { resolveBootstrapPromptTruncationWarningMode, resolveBootstrapTotalMaxChars, stripThoughtSignatures, -} from "./pi-embedded-helpers/bootstrap.js"; +} from "./embedded-agent-helpers/bootstrap.js"; export { BILLING_ERROR_USER_MESSAGE, classifyProviderRuntimeFailureKind, @@ -44,33 +44,33 @@ export { isTimeoutErrorMessage, parseImageDimensionError, parseImageSizeError, -} from "./pi-embedded-helpers/errors.js"; -export type { ProviderRuntimeFailureKind } from "./pi-embedded-helpers/errors.js"; -export { sanitizeUserFacingText } from "./pi-embedded-helpers/sanitize-user-facing-text.js"; -export { isGoogleModelApi, sanitizeGoogleTurnOrdering } from "./pi-embedded-helpers/google.js"; +} from "./embedded-agent-helpers/errors.js"; +export type { ProviderRuntimeFailureKind } from "./embedded-agent-helpers/errors.js"; +export { sanitizeUserFacingText } from "./embedded-agent-helpers/sanitize-user-facing-text.js"; +export { isGoogleModelApi, sanitizeGoogleTurnOrdering } from "./embedded-agent-helpers/google.js"; export { downgradeOpenAIFunctionCallReasoningPairs, downgradeOpenAIReasoningBlocks, -} from "./pi-embedded-helpers/openai.js"; +} from "./embedded-agent-helpers/openai.js"; export { isEmptyAssistantMessageContent, sanitizeSessionMessagesImages, -} from "./pi-embedded-helpers/images.js"; +} from "./embedded-agent-helpers/images.js"; export { isMessagingToolDuplicate, isMessagingToolDuplicateNormalized, normalizeTextForComparison, -} from "./pi-embedded-helpers/messaging-dedupe.js"; +} from "./embedded-agent-helpers/messaging-dedupe.js"; -export { pickFallbackThinkingLevel } from "./pi-embedded-helpers/thinking.js"; +export { pickFallbackThinkingLevel } from "./embedded-agent-helpers/thinking.js"; export { mergeConsecutiveUserTurns, validateAnthropicTurns, validateGeminiTurns, -} from "./pi-embedded-helpers/turns.js"; -export type { EmbeddedContextFile, FailoverReason } from "./pi-embedded-helpers/types.js"; +} from "./embedded-agent-helpers/turns.js"; +export type { EmbeddedContextFile, FailoverReason } from "./embedded-agent-helpers/types.js"; export type { ToolCallIdMode } from "./tool-call-id.js"; export { isValidCloudCodeAssistToolId, sanitizeToolCallId } from "./tool-call-id.js"; diff --git a/src/agents/pi-embedded-helpers.validate-turns.test.ts b/src/agents/embedded-agent-helpers.validate-turns.test.ts similarity index 99% rename from src/agents/pi-embedded-helpers.validate-turns.test.ts rename to src/agents/embedded-agent-helpers.validate-turns.test.ts index 7b0c45c7ba1..683916f8526 100644 --- a/src/agents/pi-embedded-helpers.validate-turns.test.ts +++ b/src/agents/embedded-agent-helpers.validate-turns.test.ts @@ -1,10 +1,10 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; import { mergeConsecutiveUserTurns, validateAnthropicTurns, validateGeminiTurns, -} from "./pi-embedded-helpers.js"; +} from "./embedded-agent-helpers.js"; function asMessages(messages: unknown[]): AgentMessage[] { return messages as AgentMessage[]; diff --git a/src/agents/pi-embedded-helpers/bootstrap.test.ts b/src/agents/embedded-agent-helpers/bootstrap.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers/bootstrap.test.ts rename to src/agents/embedded-agent-helpers/bootstrap.test.ts diff --git a/src/agents/pi-embedded-helpers/bootstrap.ts b/src/agents/embedded-agent-helpers/bootstrap.ts similarity index 99% rename from src/agents/pi-embedded-helpers/bootstrap.ts rename to src/agents/embedded-agent-helpers/bootstrap.ts index 80f24537b34..d0c28f28a0e 100644 --- a/src/agents/pi-embedded-helpers/bootstrap.ts +++ b/src/agents/embedded-agent-helpers/bootstrap.ts @@ -1,11 +1,11 @@ import fs from "node:fs/promises"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { sanitizeGoogleAssistantFirstOrdering } from "../../shared/google-turn-ordering.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { truncateUtf16Safe } from "../../utils.js"; import { resolveAgentConfig } from "../agent-scope.js"; +import type { AgentMessage } from "../runtime/index.js"; import type { WorkspaceBootstrapFile } from "../workspace.js"; import type { EmbeddedContextFile } from "./types.js"; diff --git a/src/agents/pi-embedded-helpers/errors.test.ts b/src/agents/embedded-agent-helpers/errors.test.ts similarity index 98% rename from src/agents/pi-embedded-helpers/errors.test.ts rename to src/agents/embedded-agent-helpers/errors.test.ts index 86ac6faa98c..9cb35eeec2c 100644 --- a/src/agents/pi-embedded-helpers/errors.test.ts +++ b/src/agents/embedded-agent-helpers/errors.test.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE } from "../../shared/assistant-error-format.js"; diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/embedded-agent-helpers/errors.ts similarity index 99% rename from src/agents/pi-embedded-helpers/errors.ts rename to src/agents/embedded-agent-helpers/errors.ts index 4408881317e..00d354c5d46 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/embedded-agent-helpers/errors.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { @@ -131,7 +131,7 @@ export function isContextOverflowError(errorMessage?: string): boolean { (lower.includes("input length") && lower.includes("exceed") && lower.includes("context")) || (lower.includes("413") && lower.includes("too large")) || // Anthropic API and OpenAI-compatible providers (e.g. ZhipuAI/GLM) return this stop reason - // when the context window is exceeded. pi-ai surfaces it as "Unhandled stop reason: model_context_window_exceeded". + // when the context window is exceeded. shared model runtime surfaces it as "Unhandled stop reason: model_context_window_exceeded". lower.includes("context_window_exceeded") || // Chinese proxy error messages for context overflow errorMessage.includes("上下文过长") || @@ -830,7 +830,7 @@ function isBilling429MessageForProvider(raw: string, provider: string | undefine return hasProviderBilling429Override(provider) || !isAmbiguousGeneric429BalanceMessage(raw); } -// pi-ai providers throw `Error("An unknown error occurred")` provider-agnostically +// shared model runtime providers throw `Error("An unknown error occurred")` provider-agnostically // (anthropic, google, vertex, openai-completions, mistral, bedrock, etc.) when a // stream ends with stopReason === "aborted" | "error" without specific info. Treat // it as a transient transport failure so the configured fallback chain rotates diff --git a/src/agents/pi-embedded-helpers/failover-matches.test.ts b/src/agents/embedded-agent-helpers/failover-matches.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers/failover-matches.test.ts rename to src/agents/embedded-agent-helpers/failover-matches.test.ts diff --git a/src/agents/pi-embedded-helpers/failover-matches.ts b/src/agents/embedded-agent-helpers/failover-matches.ts similarity index 99% rename from src/agents/pi-embedded-helpers/failover-matches.ts rename to src/agents/embedded-agent-helpers/failover-matches.ts index e2a1a618989..92d7a4c7446 100644 --- a/src/agents/pi-embedded-helpers/failover-matches.ts +++ b/src/agents/embedded-agent-helpers/failover-matches.ts @@ -171,7 +171,7 @@ const ERROR_PATTERNS = { /^terminated$/i, /^stream_read_error$/i, /\bund_err_(?:socket|connect|headers?|body|req_content_length_mismatch|aborted|closed)\b/i, - // pi-ai's openai-codex provider surfaces `Request failed` when the HTTP + // shared model runtime's openai-codex provider surfaces `Request failed` when the HTTP // response has no body and no status text (typical of Cloudflare 502s // from the upstream Codex service). Treat it as a transport failure so // the configured fallback chain runs instead of surfacing the error. diff --git a/src/agents/pi-embedded-helpers/google.ts b/src/agents/embedded-agent-helpers/google.ts similarity index 100% rename from src/agents/pi-embedded-helpers/google.ts rename to src/agents/embedded-agent-helpers/google.ts diff --git a/src/agents/pi-embedded-helpers/images.ts b/src/agents/embedded-agent-helpers/images.ts similarity index 98% rename from src/agents/pi-embedded-helpers/images.ts rename to src/agents/embedded-agent-helpers/images.ts index d74541a1ab2..b614bd52bfe 100644 --- a/src/agents/pi-embedded-helpers/images.ts +++ b/src/agents/embedded-agent-helpers/images.ts @@ -1,5 +1,5 @@ -import type { AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-core"; import type { ImageSanitizationLimits } from "../image-sanitization.js"; +import type { AgentMessage, AgentToolResult } from "../runtime/index.js"; import type { ToolCallIdMode } from "../tool-call-id.js"; import { sanitizeToolCallIdsForCloudCodeAssist } from "../tool-call-id.js"; import { sanitizeContentBlocksImages } from "../tool-images.js"; diff --git a/src/agents/pi-embedded-helpers/messaging-dedupe.ts b/src/agents/embedded-agent-helpers/messaging-dedupe.ts similarity index 100% rename from src/agents/pi-embedded-helpers/messaging-dedupe.ts rename to src/agents/embedded-agent-helpers/messaging-dedupe.ts diff --git a/src/agents/pi-embedded-helpers/openai.ts b/src/agents/embedded-agent-helpers/openai.ts similarity index 98% rename from src/agents/pi-embedded-helpers/openai.ts rename to src/agents/embedded-agent-helpers/openai.ts index ab676979b23..8f936032bf6 100644 --- a/src/agents/pi-embedded-helpers/openai.ts +++ b/src/agents/embedded-agent-helpers/openai.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../runtime/index.js"; type OpenAIThinkingBlock = { type?: unknown; @@ -91,7 +91,7 @@ function isOpenAIToolCallType(type: unknown): boolean { * matching `reasoning` item is absent in the same assistant turn. * * When that pairing is missing, strip the `|fc_*` suffix from tool call ids so - * pi-ai omits `function_call.id` on replay. + * shared model runtime omits `function_call.id` on replay. */ export function downgradeOpenAIFunctionCallReasoningPairs( messages: AgentMessage[], diff --git a/src/agents/pi-embedded-helpers/provider-error-patterns.test.ts b/src/agents/embedded-agent-helpers/provider-error-patterns.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers/provider-error-patterns.test.ts rename to src/agents/embedded-agent-helpers/provider-error-patterns.test.ts diff --git a/src/agents/pi-embedded-helpers/provider-error-patterns.ts b/src/agents/embedded-agent-helpers/provider-error-patterns.ts similarity index 100% rename from src/agents/pi-embedded-helpers/provider-error-patterns.ts rename to src/agents/embedded-agent-helpers/provider-error-patterns.ts diff --git a/src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts b/src/agents/embedded-agent-helpers/sanitize-user-facing-text.ts similarity index 100% rename from src/agents/pi-embedded-helpers/sanitize-user-facing-text.ts rename to src/agents/embedded-agent-helpers/sanitize-user-facing-text.ts diff --git a/src/agents/pi-embedded-helpers/thinking.test.ts b/src/agents/embedded-agent-helpers/thinking.test.ts similarity index 100% rename from src/agents/pi-embedded-helpers/thinking.test.ts rename to src/agents/embedded-agent-helpers/thinking.test.ts diff --git a/src/agents/pi-embedded-helpers/thinking.ts b/src/agents/embedded-agent-helpers/thinking.ts similarity index 100% rename from src/agents/pi-embedded-helpers/thinking.ts rename to src/agents/embedded-agent-helpers/thinking.ts diff --git a/src/agents/pi-embedded-helpers/turns.ts b/src/agents/embedded-agent-helpers/turns.ts similarity index 99% rename from src/agents/pi-embedded-helpers/turns.ts rename to src/agents/embedded-agent-helpers/turns.ts index 99c7a00e970..dca2215b2c1 100644 --- a/src/agents/pi-embedded-helpers/turns.ts +++ b/src/agents/embedded-agent-helpers/turns.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import type { AgentMessage } from "../runtime/index.js"; import { extractToolCallsFromAssistant, extractToolResultId } from "../tool-call-id.js"; type AnthropicContentBlock = { diff --git a/src/agents/pi-embedded-helpers/types.ts b/src/agents/embedded-agent-helpers/types.ts similarity index 100% rename from src/agents/pi-embedded-helpers/types.ts rename to src/agents/embedded-agent-helpers/types.ts diff --git a/src/agents/embedded-pi-lsp.ts b/src/agents/embedded-agent-lsp.ts similarity index 85% rename from src/agents/embedded-pi-lsp.ts rename to src/agents/embedded-agent-lsp.ts index fcdfb543f0f..cdff49b046d 100644 --- a/src/agents/embedded-pi-lsp.ts +++ b/src/agents/embedded-agent-lsp.ts @@ -2,15 +2,15 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { BundleLspServerConfig } from "../plugins/bundle-lsp.js"; import { loadEnabledBundleLspConfig } from "../plugins/bundle-lsp.js"; -type EmbeddedPiLspConfig = { +type EmbeddedAgentLspConfig = { lspServers: Record; diagnostics: Array<{ pluginId: string; message: string }>; }; -export function loadEmbeddedPiLspConfig(params: { +export function loadEmbeddedAgentLspConfig(params: { workspaceDir: string; cfg?: OpenClawConfig; -}): EmbeddedPiLspConfig { +}): EmbeddedAgentLspConfig { const bundleLsp = loadEnabledBundleLspConfig({ workspaceDir: params.workspaceDir, cfg: params.cfg, diff --git a/src/agents/embedded-pi-mcp.ts b/src/agents/embedded-agent-mcp.ts similarity index 83% rename from src/agents/embedded-pi-mcp.ts rename to src/agents/embedded-agent-mcp.ts index 9e34104a9b8..658f9f104aa 100644 --- a/src/agents/embedded-pi-mcp.ts +++ b/src/agents/embedded-agent-mcp.ts @@ -2,15 +2,15 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { BundleMcpDiagnostic, BundleMcpServerConfig } from "../plugins/bundle-mcp.js"; import { loadMergedBundleMcpConfig } from "./bundle-mcp-config.js"; -type EmbeddedPiMcpConfig = { +type EmbeddedAgentMcpConfig = { mcpServers: Record; diagnostics: BundleMcpDiagnostic[]; }; -export function loadEmbeddedPiMcpConfig(params: { +export function loadEmbeddedAgentMcpConfig(params: { workspaceDir: string; cfg?: OpenClawConfig; -}): EmbeddedPiMcpConfig { +}): EmbeddedAgentMcpConfig { const bundleMcp = loadMergedBundleMcpConfig({ workspaceDir: params.workspaceDir, cfg: params.cfg, diff --git a/src/agents/pi-embedded-messaging.ts b/src/agents/embedded-agent-messaging.ts similarity index 100% rename from src/agents/pi-embedded-messaging.ts rename to src/agents/embedded-agent-messaging.ts diff --git a/src/agents/pi-embedded-messaging.types.ts b/src/agents/embedded-agent-messaging.types.ts similarity index 100% rename from src/agents/pi-embedded-messaging.types.ts rename to src/agents/embedded-agent-messaging.types.ts diff --git a/src/agents/pi-embedded-payloads.ts b/src/agents/embedded-agent-payloads.ts similarity index 100% rename from src/agents/pi-embedded-payloads.ts rename to src/agents/embedded-agent-payloads.ts diff --git a/src/agents/pi-embedded-runner-extraparams-moonshot.test.ts b/src/agents/embedded-agent-runner-extraparams-moonshot.test.ts similarity index 95% rename from src/agents/pi-embedded-runner-extraparams-moonshot.test.ts rename to src/agents/embedded-agent-runner-extraparams-moonshot.test.ts index 283a9c9536e..4bab467202a 100644 --- a/src/agents/pi-embedded-runner-extraparams-moonshot.test.ts +++ b/src/agents/embedded-agent-runner-extraparams-moonshot.test.ts @@ -1,11 +1,11 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { runExtraParamsPayloadCase } from "./pi-embedded-runner-extraparams.test-support.js"; -import { testing as extraParamsTesting } from "./pi-embedded-runner/extra-params.js"; import { createMoonshotThinkingWrapper, resolveMoonshotThinkingKeep, resolveMoonshotThinkingType, -} from "./pi-embedded-runner/moonshot-stream-wrappers.js"; +} from "../llm/providers/stream-wrappers/moonshot.js"; +import { runExtraParamsPayloadCase } from "./embedded-agent-runner-extraparams.test-support.js"; +import { testing as extraParamsTesting } from "./embedded-agent-runner/extra-params.js"; beforeEach(() => { extraParamsTesting.setProviderRuntimeDepsForTest({ diff --git a/src/agents/pi-embedded-runner-extraparams-openrouter.test.ts b/src/agents/embedded-agent-runner-extraparams-openrouter.test.ts similarity index 95% rename from src/agents/pi-embedded-runner-extraparams-openrouter.test.ts rename to src/agents/embedded-agent-runner-extraparams-openrouter.test.ts index 43234b3f7f4..8507c48d2f5 100644 --- a/src/agents/pi-embedded-runner-extraparams-openrouter.test.ts +++ b/src/agents/embedded-agent-runner-extraparams-openrouter.test.ts @@ -1,15 +1,15 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { runExtraParamsPayloadCase } from "./pi-embedded-runner-extraparams.test-support.js"; -import { - applyExtraParamsToAgent, - testing as extraParamsTesting, -} from "./pi-embedded-runner/extra-params.js"; import { createOpenRouterSystemCacheWrapper, createOpenRouterWrapper, isProxyReasoningUnsupported, -} from "./pi-embedded-runner/proxy-stream-wrappers.js"; +} from "../llm/providers/stream-wrappers/proxy.js"; +import { runExtraParamsPayloadCase } from "./embedded-agent-runner-extraparams.test-support.js"; +import { + applyExtraParamsToAgent, + testing as extraParamsTesting, +} from "./embedded-agent-runner/extra-params.js"; beforeEach(() => { extraParamsTesting.setProviderRuntimeDepsForTest({ diff --git a/src/agents/pi-embedded-runner-extraparams-resolve.test.ts b/src/agents/embedded-agent-runner-extraparams-resolve.test.ts similarity index 98% rename from src/agents/pi-embedded-runner-extraparams-resolve.test.ts rename to src/agents/embedded-agent-runner-extraparams-resolve.test.ts index 7bd2c6b4561..52976beb9cb 100644 --- a/src/agents/pi-embedded-runner-extraparams-resolve.test.ts +++ b/src/agents/embedded-agent-runner-extraparams-resolve.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { resolveExtraParams } from "./pi-embedded-runner/extra-params.js"; +import { resolveExtraParams } from "./embedded-agent-runner/extra-params.js"; describe("resolveExtraParams", () => { it("returns undefined with no model config", () => { diff --git a/src/agents/pi-embedded-runner-extraparams.live.test.ts b/src/agents/embedded-agent-runner-extraparams.live.test.ts similarity index 93% rename from src/agents/pi-embedded-runner-extraparams.live.test.ts rename to src/agents/embedded-agent-runner-extraparams.live.test.ts index 0e466b470f3..06da28ffcb9 100644 --- a/src/agents/pi-embedded-runner-extraparams.live.test.ts +++ b/src/agents/embedded-agent-runner-extraparams.live.test.ts @@ -1,10 +1,10 @@ -import type { Model } from "@earendil-works/pi-ai"; -import { getModel, streamSimple } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; +import { getModel, streamSimple } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; +import { applyExtraParamsToAgent } from "./embedded-agent-runner.js"; import { isLiveTestEnabled } from "./live-test-helpers.js"; import { isLiveBillingDrift } from "./live-test-provider-drift.js"; -import { applyExtraParamsToAgent } from "./pi-embedded-runner.js"; const OPENAI_KEY = process.env.OPENAI_API_KEY ?? ""; const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY ?? ""; @@ -14,7 +14,7 @@ const ANTHROPIC_LIVE = isLiveTestEnabled(["ANTHROPIC_LIVE_TEST"]); const describeLive = LIVE && OPENAI_KEY ? describe : describe.skip; const describeAnthropicLive = ANTHROPIC_LIVE && ANTHROPIC_KEY ? describe : describe.skip; -describeLive("pi embedded extra params (live)", () => { +describeLive("embedded agent extra params (live)", () => { it("applies config max_completion_tokens alias to openai streamFn", async () => { const model = getModel("openai", "gpt-5.4") as unknown as Model<"openai-completions">; @@ -102,7 +102,7 @@ describeLive("pi embedded extra params (live)", () => { }, 45_000); }); -describeAnthropicLive("pi embedded extra params (anthropic live)", () => { +describeAnthropicLive("embedded agent extra params (anthropic live)", () => { it("verifies Anthropic fast-mode service_tier semantics against the live API", async () => { const headers = { "content-type": "application/json", diff --git a/src/agents/pi-embedded-runner-extraparams.test-support.ts b/src/agents/embedded-agent-runner-extraparams.test-support.ts similarity index 82% rename from src/agents/pi-embedded-runner-extraparams.test-support.ts rename to src/agents/embedded-agent-runner-extraparams.test-support.ts index 863e6c75df2..f58071ac992 100644 --- a/src/agents/pi-embedded-runner-extraparams.test-support.ts +++ b/src/agents/embedded-agent-runner-extraparams.test-support.ts @@ -1,6 +1,6 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { Context, Model } from "@earendil-works/pi-ai"; -import { applyExtraParamsToAgent } from "./pi-embedded-runner/extra-params.js"; +import type { Context, Model } from "openclaw/plugin-sdk/llm"; +import { applyExtraParamsToAgent } from "./embedded-agent-runner/extra-params.js"; +import type { StreamFn } from "./runtime/index.js"; export function runExtraParamsPayloadCase(params: { provider: string; diff --git a/src/agents/pi-embedded-runner-extraparams.test.ts b/src/agents/embedded-agent-runner-extraparams.test.ts similarity index 98% rename from src/agents/pi-embedded-runner-extraparams.test.ts rename to src/agents/embedded-agent-runner-extraparams.test.ts index d6fa425521a..acdb62f6946 100644 --- a/src/agents/pi-embedded-runner-extraparams.test.ts +++ b/src/agents/embedded-agent-runner-extraparams.test.ts @@ -1,8 +1,8 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai"; -import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; +import type { Context, Model, SimpleStreamOptions } from "openclaw/plugin-sdk/llm"; +import { createAssistantMessageEventStream } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { testing as extraParamsTesting } from "./pi-embedded-runner/extra-params.js"; +import { testing as extraParamsTesting } from "./embedded-agent-runner/extra-params.js"; vi.mock("../plugins/provider-hook-runtime.js", () => ({ clearProviderRuntimePluginCacheForTest: vi.fn(), @@ -287,17 +287,10 @@ function createAnthropicFastModeWrapper(baseStreamFn: StreamFn | undefined, fast return createAnthropicServiceTierWrapper(baseStreamFn, fastMode ? "auto" : "standard_only"); } -import { isAnthropicBedrockModel } from "./pi-embedded-runner/anthropic-family-cache-semantics.js"; -import { createAnthropicToolPayloadCompatibilityWrapper } from "./pi-embedded-runner/anthropic-family-tool-payload-compat.js"; -import { - applyExtraParamsToAgent, - resolveAgentTransportOverride, - resolveExplicitSettingsTransport, - resolvePreparedExtraParams, -} from "./pi-embedded-runner/extra-params.js"; -import { createGoogleThinkingPayloadWrapper } from "./pi-embedded-runner/google-stream-wrappers.js"; -import { log } from "./pi-embedded-runner/logger.js"; -import { createMinimaxFastModeWrapper } from "./pi-embedded-runner/minimax-stream-wrappers.js"; +import { isAnthropicBedrockModel } from "../llm/providers/stream-wrappers/anthropic-family-cache-semantics.js"; +import { createAnthropicToolPayloadCompatibilityWrapper } from "../llm/providers/stream-wrappers/anthropic-family-tool-payload-compat.js"; +import { createGoogleThinkingPayloadWrapper } from "../llm/providers/stream-wrappers/google.js"; +import { createMinimaxFastModeWrapper } from "../llm/providers/stream-wrappers/minimax.js"; import { createCodexNativeWebSearchWrapper, createOpenAIAttributionHeadersWrapper, @@ -313,7 +306,14 @@ import { resolveOpenAIFastMode, resolveOpenAIServiceTier, resolveOpenAITextVerbosity, -} from "./pi-embedded-runner/openai-stream-wrappers.js"; +} from "../llm/providers/stream-wrappers/openai.js"; +import { + applyExtraParamsToAgent, + resolveAgentTransportOverride, + resolveExplicitSettingsTransport, + resolvePreparedExtraParams, +} from "./embedded-agent-runner/extra-params.js"; +import { log } from "./embedded-agent-runner/logger.js"; type WrapProviderStreamFnParams = Parameters< typeof import("../plugins/provider-hook-runtime.js").wrapProviderStreamFn @@ -2708,6 +2708,7 @@ describe("applyExtraParamsToAgent", () => { } as Model<"anthropic-messages">; const context: Context = { messages: [] }; + // Simulate agent runtime passing apiKey in options (API key, not OAuth token) void agent.streamFn?.(model, context, { apiKey: "sk-ant-api03-test", // pragma: allowlist secret headers: { "X-Custom": "1" }, @@ -2763,6 +2764,7 @@ describe("applyExtraParamsToAgent", () => { } as Model<"anthropic-messages">; const context: Context = { messages: [] }; + // Simulate agent runtime passing an OAuth token (sk-ant-oat-*) as apiKey void agent.streamFn?.(model, context, { apiKey: "sk-ant-oat01-test-oauth-token", // pragma: allowlist secret headers: { "X-Custom": "1" }, @@ -2770,6 +2772,7 @@ describe("applyExtraParamsToAgent", () => { expect(calls).toHaveLength(1); const betaHeader = calls[0]?.headers?.["anthropic-beta"] as string; + // Must include the OAuth-required betas so they aren't stripped by shared model runtime's mergeHeaders expect(betaHeader).toContain("oauth-2025-04-20"); expect(betaHeader).toContain("claude-code-20250219"); expect(betaHeader).not.toContain("context-1m-2025-08-07"); @@ -2790,7 +2793,7 @@ describe("applyExtraParamsToAgent", () => { }); // context1m no longer injects a beta header (GA); only the explicitly - // configured anthropicBeta entry should appear alongside pi-ai defaults. + // configured anthropicBeta entry should appear alongside shared runtime defaults. expect(headers).toEqual({ "anthropic-beta": "prompt-caching-2024-07-31,fine-grained-tool-streaming-2025-05-14,interleaved-thinking-2025-05-14,files-api-2025-04-14", @@ -3712,39 +3715,32 @@ describe("applyExtraParamsToAgent", () => { expect(payload).not.toHaveProperty("service_tier"); }); - it("warns and skips service_tier injection for invalid serviceTier values", () => { - const warnSpy = vi.spyOn(log, "warn").mockImplementation(() => undefined); - try { - const payload = runResponsesPayloadMutationCase({ - applyProvider: "openai", - applyModelId: "gpt-5.4", - cfg: { - agents: { - defaults: { - models: { - "openai/gpt-5.4": { - params: { - serviceTier: "invalid", - }, + it("skips service_tier injection for invalid serviceTier values", () => { + const payload = runResponsesPayloadMutationCase({ + applyProvider: "openai", + applyModelId: "gpt-5.4", + cfg: { + agents: { + defaults: { + models: { + "openai/gpt-5.4": { + params: { + serviceTier: "invalid", }, }, }, }, }, - model: { - api: "openai-responses", - provider: "openai", - id: "gpt-5.4", - baseUrl: "https://api.openai.com/v1", - } as unknown as Model<"openai-responses">, - }); + }, + model: { + api: "openai-responses", + provider: "openai", + id: "gpt-5.4", + baseUrl: "https://api.openai.com/v1", + } as unknown as Model<"openai-responses">, + }); - expect(payload).not.toHaveProperty("service_tier"); - expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy).toHaveBeenCalledWith("ignoring invalid OpenAI service tier param: invalid"); - } finally { - warnSpy.mockRestore(); - } + expect(payload).not.toHaveProperty("service_tier"); }); it("does not warn for valid OpenAI serviceTier values", () => { diff --git a/src/agents/pi-embedded-runner.anthropic-tool-replay.live.test.ts b/src/agents/embedded-agent-runner.anthropic-tool-replay.live.test.ts similarity index 95% rename from src/agents/pi-embedded-runner.anthropic-tool-replay.live.test.ts rename to src/agents/embedded-agent-runner.anthropic-tool-replay.live.test.ts index dba81e94e95..d53fd975baf 100644 --- a/src/agents/pi-embedded-runner.anthropic-tool-replay.live.test.ts +++ b/src/agents/embedded-agent-runner.anthropic-tool-replay.live.test.ts @@ -1,13 +1,13 @@ -import type { Message, Model } from "@earendil-works/pi-ai"; +import type { Message, Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; +import { wrapStreamFnSanitizeMalformedToolCalls } from "./embedded-agent-runner/run/attempt.tool-call-normalization.js"; +import { OMITTED_ASSISTANT_REASONING_TEXT } from "./embedded-agent-runner/thinking.js"; import { completeSimpleWithLiveTimeout, extractAssistantText, logLiveCache, } from "./live-cache-test-support.js"; import { isLiveTestEnabled } from "./live-test-helpers.js"; -import { wrapStreamFnSanitizeMalformedToolCalls } from "./pi-embedded-runner/run/attempt.tool-call-normalization.js"; -import { OMITTED_ASSISTANT_REASONING_TEXT } from "./pi-embedded-runner/thinking.js"; import { buildAssistantMessageWithZeroUsage } from "./stream-message-shared.js"; const ANTHROPIC_LIVE = isLiveTestEnabled(["ANTHROPIC_LIVE_TEST"]); @@ -52,7 +52,7 @@ function buildLiveAnthropicModel(): { }; } -describeLive("pi embedded anthropic replay sanitization (live)", () => { +describeLive("embedded agent anthropic replay sanitization (live)", () => { it( "accepts regular text-only assistant replay history", async () => { diff --git a/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts b/src/agents/embedded-agent-runner.buildembeddedsandboxinfo.test.ts similarity index 96% rename from src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts rename to src/agents/embedded-agent-runner.buildembeddedsandboxinfo.test.ts index 4a43112e810..9d615b6ea93 100644 --- a/src/agents/pi-embedded-runner.buildembeddedsandboxinfo.test.ts +++ b/src/agents/embedded-agent-runner.buildembeddedsandboxinfo.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { buildEmbeddedSandboxInfo } from "./pi-embedded-runner.js"; -import { resolveEmbeddedFullAccessState } from "./pi-embedded-runner/sandbox-info.js"; +import { buildEmbeddedSandboxInfo } from "./embedded-agent-runner.js"; +import { resolveEmbeddedFullAccessState } from "./embedded-agent-runner/sandbox-info.js"; import type { SandboxContext } from "./sandbox.js"; function createSandboxContext(overrides?: Partial): SandboxContext { diff --git a/src/agents/pi-embedded-runner.cache.live.test.ts b/src/agents/embedded-agent-runner.cache.live.test.ts similarity index 99% rename from src/agents/pi-embedded-runner.cache.live.test.ts rename to src/agents/embedded-agent-runner.cache.live.test.ts index 55cb22637cc..0218fe246d7 100644 --- a/src/agents/pi-embedded-runner.cache.live.test.ts +++ b/src/agents/embedded-agent-runner.cache.live.test.ts @@ -1,10 +1,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AssistantMessage, Message, Tool } from "@earendil-works/pi-ai"; +import type { AssistantMessage, Message, Tool } from "openclaw/plugin-sdk/llm"; import { Type } from "typebox"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; +import { runEmbeddedAgent } from "./embedded-agent-runner.js"; +import { compactEmbeddedAgentSessionDirect } from "./embedded-agent-runner/compact.runtime.js"; import { buildAssistantHistoryTurn as buildTypedAssistantHistoryTurn, buildStableCachePrefix, @@ -16,8 +18,6 @@ import { resolveLiveDirectModel, withLiveCacheHeartbeat, } from "./live-cache-test-support.js"; -import { runEmbeddedPiAgent } from "./pi-embedded-runner.js"; -import { compactEmbeddedPiSessionDirect } from "./pi-embedded-runner/compact.runtime.js"; import { buildZeroUsage } from "./stream-message-shared.js"; const describeCacheLive = LIVE_CACHE_TEST_ENABLED ? describe : describe.skip; @@ -310,7 +310,7 @@ async function runEmbeddedCacheProbe(params: { const sessionPaths = buildRunnerSessionPaths(params.sessionId); await fs.mkdir(sessionPaths.workspaceDir, { recursive: true }); const result = await withLiveCacheHeartbeat( - runEmbeddedPiAgent({ + runEmbeddedAgent({ sessionId: params.sessionId, sessionKey: `live-cache:${params.providerTag}:${params.sessionId}`, sessionFile: sessionPaths.sessionFile, @@ -354,7 +354,7 @@ async function compactLiveCacheSession(params: { const sessionPaths = buildRunnerSessionPaths(params.sessionId); await fs.mkdir(sessionPaths.workspaceDir, { recursive: true }); return await withLiveCacheHeartbeat( - compactEmbeddedPiSessionDirect({ + compactEmbeddedAgentSessionDirect({ sessionId: params.sessionId, sessionKey: `live-cache:${params.providerTag}:${params.sessionId}`, sessionFile: sessionPaths.sessionFile, @@ -752,7 +752,7 @@ async function runAnthropicImageCacheProbe(params: { }; } -describeCacheLive("pi embedded runner prompt caching (live)", () => { +describeCacheLive("embedded agent runner prompt caching (live)", () => { beforeAll(async () => { liveRunnerRootDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-live-cache-")); liveCacheTraceFile = path.join(liveRunnerRootDir, "cache-trace.jsonl"); diff --git a/src/agents/pi-embedded-runner.compaction-safety-timeout.test.ts b/src/agents/embedded-agent-runner.compaction-safety-timeout.test.ts similarity index 99% rename from src/agents/pi-embedded-runner.compaction-safety-timeout.test.ts rename to src/agents/embedded-agent-runner.compaction-safety-timeout.test.ts index 7938ba424fc..d986c34a635 100644 --- a/src/agents/pi-embedded-runner.compaction-safety-timeout.test.ts +++ b/src/agents/embedded-agent-runner.compaction-safety-timeout.test.ts @@ -5,7 +5,7 @@ import { compactWithSafetyTimeout, EMBEDDED_COMPACTION_TIMEOUT_MS, resolveCompactionTimeoutMs, -} from "./pi-embedded-runner/compaction-safety-timeout.js"; +} from "./embedded-agent-runner/compaction-safety-timeout.js"; describe("compactWithSafetyTimeout", () => { beforeEach(() => { diff --git a/src/agents/pi-embedded-runner.createsystempromptoverride.test.ts b/src/agents/embedded-agent-runner.createsystempromptoverride.test.ts similarity index 85% rename from src/agents/pi-embedded-runner.createsystempromptoverride.test.ts rename to src/agents/embedded-agent-runner.createsystempromptoverride.test.ts index 439ba9148a0..f0fb97973b3 100644 --- a/src/agents/pi-embedded-runner.createsystempromptoverride.test.ts +++ b/src/agents/embedded-agent-runner.createsystempromptoverride.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { createSystemPromptOverride } from "./pi-embedded-runner.js"; +import { createSystemPromptOverride } from "./embedded-agent-runner.js"; describe("createSystemPromptOverride", () => { it("returns the override prompt trimmed", () => { diff --git a/src/agents/pi-embedded-runner.e2e.test.ts b/src/agents/embedded-agent-runner.e2e.test.ts similarity index 87% rename from src/agents/pi-embedded-runner.e2e.test.ts rename to src/agents/embedded-agent-runner.e2e.test.ts index 6d9cc615487..00310e45d64 100644 --- a/src/agents/pi-embedded-runner.e2e.test.ts +++ b/src/agents/embedded-agent-runner.e2e.test.ts @@ -4,19 +4,19 @@ import "./test-helpers/fast-coding-tools.js"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { buildEmbeddedRunnerAssistant, - cleanupEmbeddedPiRunnerTestWorkspace, + cleanupEmbeddedAgentRunnerTestWorkspace, createMockUsage, - createEmbeddedPiRunnerOpenAiConfig, + createEmbeddedAgentRunnerOpenAiConfig, createResolvedEmbeddedRunnerModel, - createEmbeddedPiRunnerTestWorkspace, - type EmbeddedPiRunnerTestWorkspace, + createEmbeddedAgentRunnerTestWorkspace, + type EmbeddedAgentRunnerTestWorkspace, immediateEnqueue, makeEmbeddedRunnerAttempt, -} from "./test-helpers/pi-embedded-runner-e2e-fixtures.js"; +} from "./test-helpers/embedded-agent-runner-e2e-fixtures.js"; import { installEmbeddedRunnerBaseE2eMocks, installEmbeddedRunnerFastRunE2eMocks, -} from "./test-helpers/pi-embedded-runner-e2e-mocks.js"; +} from "./test-helpers/embedded-agent-runner-e2e-mocks.js"; const runEmbeddedAttemptMock = vi.fn(); const disposeSessionMcpRuntimeMock = vi.fn<(sessionId: string) => Promise>(async () => { @@ -31,9 +31,9 @@ const ensureOpenClawModelsJsonMock = vi.fn(async () => ({ wrote: false })); const loggerWarnMock = vi.fn(); let refreshRuntimeAuthOnFirstPromptError = false; -vi.mock("@earendil-works/pi-ai", async () => { +vi.mock("openclaw/plugin-sdk/llm", async () => { const actual = - await vi.importActual("@earendil-works/pi-ai"); + await vi.importActual("openclaw/plugin-sdk/llm"); const buildAssistantMessage = (model: { api: string; provider: string; id: string }) => ({ role: "assistant" as const, @@ -105,9 +105,9 @@ const installRunEmbeddedMocks = () => { resolveStoredSessionKeyForSessionIdMock(opts), }; }); - vi.doMock("./pi-embedded-runner/logger.js", async () => { - const actual = await vi.importActual( - "./pi-embedded-runner/logger.js", + vi.doMock("./embedded-agent-runner/logger.js", async () => { + const actual = await vi.importActual( + "./embedded-agent-runner/logger.js", ); return { ...actual, @@ -117,15 +117,15 @@ const installRunEmbeddedMocks = () => { }, }; }); - vi.doMock("./pi-bundle-mcp-tools.js", () => ({ + vi.doMock("./agent-bundle-mcp-tools.js", () => ({ disposeSessionMcpRuntime: (sessionId: string) => disposeSessionMcpRuntimeMock(sessionId), retireSessionMcpRuntimeForSessionKey: () => Promise.resolve(false), retireSessionMcpRuntime: ({ sessionId }: { sessionId?: string | null }) => sessionId ? disposeSessionMcpRuntimeMock(sessionId) : Promise.resolve(false), })); - vi.doMock("./pi-embedded-runner/model.js", async () => { - const actual = await vi.importActual( - "./pi-embedded-runner/model.js", + vi.doMock("./embedded-agent-runner/model.js", async () => { + const actual = await vi.importActual( + "./embedded-agent-runner/model.js", ); return { ...actual, @@ -133,7 +133,7 @@ const installRunEmbeddedMocks = () => { resolveModelAsyncMock(...args), }; }); - vi.doMock("./pi-embedded-runner/run/auth-controller.js", () => ({ + vi.doMock("./embedded-agent-runner/run/auth-controller.js", () => ({ createEmbeddedRunAuthController: () => ({ advanceAuthProfile: vi.fn(async () => false), initializeAuthProfile: vi.fn(async () => undefined), @@ -153,9 +153,9 @@ const installRunEmbeddedMocks = () => { }); }; -let runEmbeddedPiAgent: typeof import("./pi-embedded-runner/run.js").runEmbeddedPiAgent; -let SessionManager: typeof import("@earendil-works/pi-coding-agent").SessionManager; -let e2eWorkspace: EmbeddedPiRunnerTestWorkspace | undefined; +let runEmbeddedAgent: typeof import("./embedded-agent-runner/run.js").runEmbeddedAgent; +let SessionManager: typeof import("openclaw/plugin-sdk/agent-sessions").SessionManager; +let e2eWorkspace: EmbeddedAgentRunnerTestWorkspace | undefined; let agentDir: string; let workspaceDir: string; let sessionCounter = 0; @@ -165,14 +165,14 @@ beforeAll(async () => { vi.useRealTimers(); vi.resetModules(); installRunEmbeddedMocks(); - ({ runEmbeddedPiAgent } = await import("./pi-embedded-runner/run.js")); - ({ SessionManager } = await import("@earendil-works/pi-coding-agent")); - e2eWorkspace = await createEmbeddedPiRunnerTestWorkspace("openclaw-embedded-agent-"); + ({ runEmbeddedAgent } = await import("./embedded-agent-runner/run.js")); + ({ SessionManager } = await import("openclaw/plugin-sdk/agent-sessions")); + e2eWorkspace = await createEmbeddedAgentRunnerTestWorkspace("openclaw-embedded-agent-"); ({ agentDir, workspaceDir } = e2eWorkspace); }, 180_000); afterAll(async () => { - await cleanupEmbeddedPiRunnerTestWorkspace(e2eWorkspace); + await cleanupEmbeddedAgentRunnerTestWorkspace(e2eWorkspace); e2eWorkspace = undefined; }); @@ -220,8 +220,8 @@ const runWithOrphanedSingleUserMessage = async (text: string, sessionKey: string }), ); - const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); - return await runEmbeddedPiAgent({ + const cfg = createEmbeddedAgentRunnerOpenAiConfig(["mock-1"]); + return await runEmbeddedAgent({ sessionId: "session:test", sessionKey, sessionFile, @@ -268,7 +268,7 @@ const readSessionMessages = async (sessionFile: string) => { }; const runDefaultEmbeddedTurn = async (sessionFile: string, prompt: string, sessionKey: string) => { - const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-error"]); + const cfg = createEmbeddedAgentRunnerOpenAiConfig(["mock-error"]); runEmbeddedAttemptMock.mockResolvedValueOnce( makeEmbeddedRunnerAttempt({ assistantTexts: ["ok"], @@ -277,7 +277,7 @@ const runDefaultEmbeddedTurn = async (sessionFile: string, prompt: string, sessi }), }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "session:test", sessionKey, sessionFile, @@ -305,10 +305,10 @@ function firstRunEmbeddedAttemptParams(): { sessionKey?: string } { return firstMockCall(runEmbeddedAttemptMock, "embedded attempt")[0] as { sessionKey?: string }; } -describe("runEmbeddedPiAgent", () => { +describe("runEmbeddedAgent", () => { it("skips models.json generation when dynamic model resolution succeeds", async () => { const sessionFile = nextSessionFile(); - const cfg = createEmbeddedPiRunnerOpenAiConfig([]); + const cfg = createEmbeddedAgentRunnerOpenAiConfig([]); runEmbeddedAttemptMock.mockResolvedValueOnce( makeEmbeddedRunnerAttempt({ assistantTexts: ["ok"], @@ -318,7 +318,7 @@ describe("runEmbeddedPiAgent", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "dynamic-model", sessionFile, workspaceDir, @@ -338,14 +338,14 @@ describe("runEmbeddedPiAgent", () => { expect(resolveModelCall?.[2]).toBe(agentDir); expect(resolveModelCall?.[3]).toBe(cfg); expect( - (resolveModelCall?.[4] as { skipPiDiscovery?: boolean } | undefined)?.skipPiDiscovery, + (resolveModelCall?.[4] as { skipAgentDiscovery?: boolean } | undefined)?.skipAgentDiscovery, ).toBe(true); expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled(); }); - it("resolves explicit OpenAI PI runs through Codex when auth order starts with Codex OAuth", async () => { + it("resolves explicit OpenAI OpenClaw runs through Codex when auth order starts with Codex OAuth", async () => { const sessionFile = nextSessionFile(); - const baseConfig = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); + const baseConfig = createEmbeddedAgentRunnerOpenAiConfig(["mock-1"]); const openAIProvider = baseConfig.models?.providers?.openai; if (!openAIProvider) { throw new Error("expected OpenAI provider test config"); @@ -364,7 +364,7 @@ describe("runEmbeddedPiAgent", () => { defaults: { models: { "openai/mock-1": { - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, }, }, }, @@ -384,8 +384,8 @@ describe("runEmbeddedPiAgent", () => { }), ); - await runEmbeddedPiAgent({ - sessionId: "codex-first-pi", + await runEmbeddedAgent({ + sessionId: "codex-first-openclaw", sessionFile, workspaceDir, config: cfg, @@ -394,7 +394,7 @@ describe("runEmbeddedPiAgent", () => { model: "mock-1", timeoutMs: 5_000, agentDir, - runId: nextRunId("codex-first-pi"), + runId: nextRunId("codex-first-openclaw"), enqueue: immediateEnqueue, }); @@ -404,7 +404,7 @@ describe("runEmbeddedPiAgent", () => { "mock-1", agentDir, cfg, - expect.objectContaining({ skipPiDiscovery: true }), + expect.objectContaining({ skipAgentDiscovery: true }), ); expect(resolveModelAsyncMock).toHaveBeenNthCalledWith( 2, @@ -412,7 +412,7 @@ describe("runEmbeddedPiAgent", () => { "mock-1", agentDir, cfg, - expect.objectContaining({ skipPiDiscovery: true }), + expect.objectContaining({ skipAgentDiscovery: true }), ); expect( (firstRunEmbeddedAttemptParams() as { model?: { provider?: string } }).model?.provider, @@ -421,7 +421,7 @@ describe("runEmbeddedPiAgent", () => { it("backfills a trimmed session key from sessionId when the embedded run omits it", async () => { const sessionFile = nextSessionFile(); - const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); + const cfg = createEmbeddedAgentRunnerOpenAiConfig(["mock-1"]); resolveSessionKeyForRequestMock.mockReturnValue({ sessionKey: "agent:test:resolved", sessionStore: {}, @@ -436,7 +436,7 @@ describe("runEmbeddedPiAgent", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "resume-123", sessionKey: " ", sessionFile, @@ -462,7 +462,7 @@ describe("runEmbeddedPiAgent", () => { it("drops whitespace-only session keys when backfill cannot resolve a session key", async () => { const sessionFile = nextSessionFile(); - const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); + const cfg = createEmbeddedAgentRunnerOpenAiConfig(["mock-1"]); resolveSessionKeyForRequestMock.mockReturnValue({ sessionKey: undefined, sessionStore: {}, @@ -477,7 +477,7 @@ describe("runEmbeddedPiAgent", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "resume-124", sessionKey: " ", sessionFile, @@ -503,7 +503,7 @@ describe("runEmbeddedPiAgent", () => { it("logs when embedded session-key backfill resolution fails", async () => { const sessionFile = nextSessionFile(); - const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); + const cfg = createEmbeddedAgentRunnerOpenAiConfig(["mock-1"]); resolveSessionKeyForRequestMock.mockImplementation(() => { throw new Error("resolver exploded"); }); @@ -516,7 +516,7 @@ describe("runEmbeddedPiAgent", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "resume-456", sessionFile, workspaceDir, @@ -539,7 +539,7 @@ describe("runEmbeddedPiAgent", () => { it("passes the current agentId when backfilling a session key", async () => { const sessionFile = nextSessionFile(); - const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); + const cfg = createEmbeddedAgentRunnerOpenAiConfig(["mock-1"]); resolveStoredSessionKeyForSessionIdMock.mockReturnValue({ sessionKey: "agent:test:resolved", sessionStore: {}, @@ -554,7 +554,7 @@ describe("runEmbeddedPiAgent", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "resume-agent-1", sessionKey: undefined, sessionFile, @@ -580,7 +580,7 @@ describe("runEmbeddedPiAgent", () => { it("disposes bundle MCP once when a one-shot local run completes", async () => { const sessionFile = nextSessionFile(); - const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); + const cfg = createEmbeddedAgentRunnerOpenAiConfig(["mock-1"]); const sessionKey = nextSessionKey(); runEmbeddedAttemptMock.mockResolvedValueOnce( makeEmbeddedRunnerAttempt({ @@ -591,7 +591,7 @@ describe("runEmbeddedPiAgent", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "session:test", sessionKey, sessionFile, @@ -615,7 +615,7 @@ describe("runEmbeddedPiAgent", () => { it("preserves bundle MCP state across retries within one local run", async () => { refreshRuntimeAuthOnFirstPromptError = true; const sessionFile = nextSessionFile(); - const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]); + const cfg = createEmbeddedAgentRunnerOpenAiConfig(["mock-1"]); const sessionKey = nextSessionKey(); runEmbeddedAttemptMock .mockImplementationOnce(async () => { @@ -634,7 +634,7 @@ describe("runEmbeddedPiAgent", () => { }); }); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ sessionId: "session:test", sessionKey, sessionFile, @@ -658,7 +658,7 @@ describe("runEmbeddedPiAgent", () => { it("retries a planning-only GPT turn once with an act-now steer", async () => { const sessionFile = nextSessionFile(); - const cfg = createEmbeddedPiRunnerOpenAiConfig(["gpt-5.4"]); + const cfg = createEmbeddedAgentRunnerOpenAiConfig(["gpt-5.4"]); const sessionKey = nextSessionKey(); runEmbeddedAttemptMock @@ -690,7 +690,7 @@ describe("runEmbeddedPiAgent", () => { }); }); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ sessionId: "session:test", sessionKey, sessionFile, @@ -711,7 +711,7 @@ describe("runEmbeddedPiAgent", () => { it("handles prompt error paths without dropping user state", async () => { const sessionFile = nextSessionFile(); - const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-error"]); + const cfg = createEmbeddedAgentRunnerOpenAiConfig(["mock-error"]); const sessionKey = nextSessionKey(); runEmbeddedAttemptMock.mockResolvedValueOnce( makeEmbeddedRunnerAttempt({ @@ -719,7 +719,7 @@ describe("runEmbeddedPiAgent", () => { }), ); await expect( - runEmbeddedPiAgent({ + runEmbeddedAgent({ sessionId: "session:test", sessionKey, sessionFile, diff --git a/src/agents/pi-embedded-runner.extensions.test.ts b/src/agents/embedded-agent-runner.extensions.test.ts similarity index 95% rename from src/agents/pi-embedded-runner.extensions.test.ts rename to src/agents/embedded-agent-runner.extensions.test.ts index a88bb9cf69a..277fdbb7180 100644 --- a/src/agents/pi-embedded-runner.extensions.test.ts +++ b/src/agents/embedded-agent-runner.extensions.test.ts @@ -1,8 +1,8 @@ -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, describe, expect, it } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; -import { buildEmbeddedExtensionFactories } from "./pi-embedded-runner/extensions.js"; +import { buildEmbeddedExtensionFactories } from "./embedded-agent-runner/extensions.js"; import { cleanupTempPluginTestEnvironment } from "./test-helpers/temp-plugin-extension-fixtures.js"; const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; @@ -25,7 +25,7 @@ describe("buildEmbeddedExtensionFactories", () => { event.result.content = [{ type: "text", text: `compacted ${seenToolCallIds.length}` }]; return undefined; }, - runtimes: ["pi"], + runtimes: ["openclaw"], source: "test", }); setActivePluginRegistry(registry); @@ -65,8 +65,8 @@ describe("buildEmbeddedExtensionFactories", () => { details: {}, }); expect(seenToolCallIds).toHaveLength(2); - expect(seenToolCallIds[0]).toMatch(/^pi-/); - expect(seenToolCallIds[1]).toMatch(/^pi-/); + expect(seenToolCallIds[0]).toMatch(/^openclaw-/); + expect(seenToolCallIds[1]).toMatch(/^openclaw-/); expect(seenToolCallIds[0]).not.toBe(seenToolCallIds[1]); }); @@ -124,7 +124,7 @@ describe("buildEmbeddedExtensionFactories", () => { event.result.details = { redacted: true }; return undefined; }, - runtimes: ["pi"], + runtimes: ["openclaw"], source: "test", }); setActivePluginRegistry(registry); diff --git a/src/agents/pi-embedded-runner.guard.test.ts b/src/agents/embedded-agent-runner.guard.test.ts similarity index 98% rename from src/agents/pi-embedded-runner.guard.test.ts rename to src/agents/embedded-agent-runner.guard.test.ts index b3af06e19fc..e5054649e93 100644 --- a/src/agents/pi-embedded-runner.guard.test.ts +++ b/src/agents/embedded-agent-runner.guard.test.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { guardSessionManager } from "./session-tool-result-guard-wrapper.js"; diff --git a/src/agents/pi-embedded-runner.guard.waitforidle-before-flush.test.ts b/src/agents/embedded-agent-runner.guard.waitforidle-before-flush.test.ts similarity index 96% rename from src/agents/pi-embedded-runner.guard.waitforidle-before-flush.test.ts rename to src/agents/embedded-agent-runner.guard.waitforidle-before-flush.test.ts index 9b96e4d5e04..650d1546fc5 100644 --- a/src/agents/pi-embedded-runner.guard.waitforidle-before-flush.test.ts +++ b/src/agents/embedded-agent-runner.guard.waitforidle-before-flush.test.ts @@ -1,7 +1,7 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { flushPendingToolResultsAfterIdle } from "./pi-embedded-runner/wait-for-idle-before-flush.js"; +import { flushPendingToolResultsAfterIdle } from "./embedded-agent-runner/wait-for-idle-before-flush.js"; import { guardSessionManager } from "./session-tool-result-guard-wrapper.js"; function assistantToolCall(id: string): AgentMessage { diff --git a/src/agents/pi-embedded-runner.limithistoryturns.test.ts b/src/agents/embedded-agent-runner.limithistoryturns.test.ts similarity index 96% rename from src/agents/pi-embedded-runner.limithistoryturns.test.ts rename to src/agents/embedded-agent-runner.limithistoryturns.test.ts index 0cd5ccfc79e..1b217d65ad5 100644 --- a/src/agents/pi-embedded-runner.limithistoryturns.test.ts +++ b/src/agents/embedded-agent-runner.limithistoryturns.test.ts @@ -1,6 +1,6 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; -import { limitHistoryTurns } from "./pi-embedded-runner/history.js"; +import { limitHistoryTurns } from "./embedded-agent-runner/history.js"; describe("limitHistoryTurns", () => { const mockUsage = { diff --git a/src/agents/pi-embedded-runner.openai-tool-id-preservation.test.ts b/src/agents/embedded-agent-runner.openai-tool-id-preservation.test.ts similarity index 95% rename from src/agents/pi-embedded-runner.openai-tool-id-preservation.test.ts rename to src/agents/embedded-agent-runner.openai-tool-id-preservation.test.ts index 3afa46cd71e..54f9048c4fd 100644 --- a/src/agents/pi-embedded-runner.openai-tool-id-preservation.test.ts +++ b/src/agents/embedded-agent-runner.openai-tool-id-preservation.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { beforeAll, describe, expect, it, vi } from "vitest"; import { createSanitizeSessionHistoryHelpersMock, @@ -8,10 +8,10 @@ import { makeInMemorySessionManager, makeModelSnapshotEntry, type SanitizeSessionHistoryHarness, -} from "./pi-embedded-runner.sanitize-session-history.test-harness.js"; +} from "./embedded-agent-runner.sanitize-session-history.test-harness.js"; import { castAgentMessage } from "./test-helpers/agent-message-fixtures.js"; -vi.mock("./pi-embedded-helpers.js", async () => await createSanitizeSessionHistoryHelpersMock()); +vi.mock("./embedded-agent-helpers.js", async () => await createSanitizeSessionHistoryHelpersMock()); vi.mock( "../plugins/provider-runtime.js", diff --git a/src/agents/pi-embedded-runner.resolvesessionagentids.test.ts b/src/agents/embedded-agent-runner.resolvesessionagentids.test.ts similarity index 100% rename from src/agents/pi-embedded-runner.resolvesessionagentids.test.ts rename to src/agents/embedded-agent-runner.resolvesessionagentids.test.ts diff --git a/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts b/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts similarity index 96% rename from src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts rename to src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts index 8c47e335c89..f5de9c421ba 100644 --- a/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts +++ b/src/agents/embedded-agent-runner.run-embedded-agent.auth-profile-rotation.e2e.test.ts @@ -1,18 +1,18 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { redactIdentifier } from "../logging/redact-identifier.js"; import type { AuthProfileFailureReason } from "./auth-profiles.js"; -import { buildAttemptReplayMetadata } from "./pi-embedded-runner/run/incomplete-turn.js"; -import type { EmbeddedRunAttemptResult } from "./pi-embedded-runner/run/types.js"; +import { buildAttemptReplayMetadata } from "./embedded-agent-runner/run/incomplete-turn.js"; +import type { EmbeddedRunAttemptResult } from "./embedded-agent-runner/run/types.js"; import { installEmbeddedRunnerBackoffE2eMocks, installEmbeddedRunnerBaseE2eMocks, installEmbeddedRunnerFastRunE2eMocks, -} from "./test-helpers/pi-embedded-runner-e2e-mocks.js"; +} from "./test-helpers/embedded-agent-runner-e2e-mocks.js"; const runEmbeddedAttemptMock = vi.fn<(params: unknown) => Promise>(); const resolveCopilotApiTokenMock = vi.fn(); @@ -42,7 +42,7 @@ const installRunEmbeddedMocks = () => { }; }, }); - vi.doMock("./pi-embedded-runner/model.js", () => ({ + vi.doMock("./embedded-agent-runner/model.js", () => ({ resolveModelAsync: async (provider: string, modelId: string) => ({ model: { id: modelId, @@ -68,8 +68,8 @@ const installRunEmbeddedMocks = () => { computeBackoff: (policy, attempt) => computeBackoffMock(policy, attempt), sleepWithAbort: (ms, abortSignal) => sleepWithAbortMock(ms, abortSignal), }); - vi.doMock("./pi-embedded-runner/compact.js", () => ({ - compactEmbeddedPiSessionDirect: vi.fn(async () => { + vi.doMock("./embedded-agent-runner/compact.js", () => ({ + compactEmbeddedAgentSessionDirect: vi.fn(async () => { throw new Error("compact should not run in auth profile rotation tests"); }), })); @@ -82,7 +82,7 @@ const installRunEmbeddedMocks = () => { }); }; -let runEmbeddedPiAgent: typeof import("./pi-embedded-runner/run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./embedded-agent-runner/run.js").runEmbeddedAgent; let authProfileUsageTesting: typeof import("./auth-profiles/usage.js").testing; let createDiagnosticLogRecordCaptureFn: typeof import("../logging/test-helpers/diagnostic-log-capture.js").createDiagnosticLogRecordCapture; let cleanupLogCapture: (() => void) | undefined; @@ -93,7 +93,7 @@ const originalFetch = globalThis.fetch; beforeAll(async () => { vi.resetModules(); installRunEmbeddedMocks(); - ({ runEmbeddedPiAgent } = await import("./pi-embedded-runner/run.js")); + ({ runEmbeddedAgent } = await import("./embedded-agent-runner/run.js")); ({ testing: authProfileUsageTesting } = await import("./auth-profiles/usage.js")); ({ createDiagnosticLogRecordCapture: createDiagnosticLogRecordCaptureFn } = await import("../logging/test-helpers/diagnostic-log-capture.js")); @@ -101,10 +101,10 @@ beforeAll(async () => { await import("../logging/logger.js")); }); -async function runEmbeddedPiAgentInline( - params: Parameters[0], -): Promise>> { - return await runEmbeddedPiAgent({ +async function runEmbeddedAgentInline( + params: Parameters[0], +): Promise>> { + return await runEmbeddedAgent({ ...params, enqueue: async (task) => await task(), }); @@ -444,7 +444,7 @@ async function runAutoPinnedOpenAiTurn(params: { authProfileId?: string; config?: OpenClawConfig; }) { - await runEmbeddedPiAgentInline({ + await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: params.sessionKey, sessionFile: path.join(params.workspaceDir, "session.jsonl"), @@ -665,7 +665,7 @@ async function runTurnWithCooldownSeed(params: { }); mockSingleSuccessfulAttempt(); - await runEmbeddedPiAgentInline({ + await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: params.sessionKey, sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -686,7 +686,7 @@ async function runTurnWithCooldownSeed(params: { }); } -describe("runEmbeddedPiAgent auth profile rotation", () => { +describe("runEmbeddedAgent auth profile rotation", () => { it("refreshes copilot token after auth error and retries once", async () => { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-")); const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-")); @@ -730,7 +730,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }), ); - await runEmbeddedPiAgentInline({ + await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:copilot-auth-error", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -815,7 +815,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }), ); - await runEmbeddedPiAgentInline({ + await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:copilot-auth-repeat", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -863,7 +863,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }), ); - const runPromise = runEmbeddedPiAgentInline({ + const runPromise = runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:copilot-shutdown", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1067,7 +1067,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }), ); - const result = await runEmbeddedPiAgentInline({ + const result = await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:compaction-timeout", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1106,7 +1106,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }), ); - const result = await runEmbeddedPiAgentInline({ + const result = await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:compaction-wait-abort", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1135,7 +1135,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { mockSingleErrorAttempt({ errorMessage: "rate limit" }); await expectFailoverError( - runEmbeddedPiAgentInline({ + runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:user", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1185,7 +1185,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }); mockSingleSuccessfulAttempt(); - await runEmbeddedPiAgentInline({ + await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:user-order-excluded", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1214,7 +1214,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { await writeOpenAiCodexAuthStore(agentDir); mockSingleSuccessfulAttempt(); - await runEmbeddedPiAgentInline({ + await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:user-auth-alias", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1255,7 +1255,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }), ); - await runEmbeddedPiAgentInline({ + await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:mismatch", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1297,7 +1297,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }); await expectFailoverError( - runEmbeddedPiAgentInline({ + runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:cooldown-failover", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1341,7 +1341,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }), ); - const result = await runEmbeddedPiAgentInline({ + const result = await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:cooldown-probe", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1389,7 +1389,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }), ); - const result = await runEmbeddedPiAgentInline({ + const result = await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:overloaded-cooldown-probe", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1437,7 +1437,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }), ); - const result = await runEmbeddedPiAgentInline({ + const result = await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:billing-cooldown-probe-no-fallbacks", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1468,7 +1468,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }); await expectFailoverError( - runEmbeddedPiAgentInline({ + runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:support:cooldown-failover", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1513,7 +1513,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { }); await expectFailoverError( - runEmbeddedPiAgentInline({ + runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:disabled-failover", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1549,7 +1549,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { await fs.writeFile(authStatePath, JSON.stringify({ version: 1, usageStats: {} })); await expectFailoverError( - runEmbeddedPiAgentInline({ + runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:auth-unavailable", sessionFile: path.join(workspaceDir, "session.jsonl"), @@ -1588,7 +1588,7 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { let thrown: unknown; try { - await runEmbeddedPiAgentInline({ + await runEmbeddedAgentInline({ sessionId: "session:test", sessionKey: "agent:test:billing-failover-active-model", sessionFile: path.join(workspaceDir, "session.jsonl"), diff --git a/src/agents/pi-embedded-runner.sanitize-session-history.policy.test.ts b/src/agents/embedded-agent-runner.sanitize-session-history.policy.test.ts similarity index 96% rename from src/agents/pi-embedded-runner.sanitize-session-history.policy.test.ts rename to src/agents/embedded-agent-runner.sanitize-session-history.policy.test.ts index e0d852712e0..ce74e73b92f 100644 --- a/src/agents/pi-embedded-runner.sanitize-session-history.policy.test.ts +++ b/src/agents/embedded-agent-runner.sanitize-session-history.policy.test.ts @@ -9,11 +9,11 @@ import { type SanitizeSessionHistoryHarness, sanitizeSnapshotChangedOpenAIReasoning, sanitizeWithOpenAIResponses, -} from "./pi-embedded-runner.sanitize-session-history.test-harness.js"; +} from "./embedded-agent-runner.sanitize-session-history.test-harness.js"; import { makeZeroUsageSnapshot } from "./usage.js"; vi.mock( - "./pi-embedded-helpers.js", + "./embedded-agent-helpers.js", async () => await createSanitizeSessionHistoryHelpersMock({ isGoogleModelApi: vi.fn() }), ); diff --git a/src/agents/pi-embedded-runner.sanitize-session-history.test-harness.ts b/src/agents/embedded-agent-runner.sanitize-session-history.test-harness.ts similarity index 93% rename from src/agents/pi-embedded-runner.sanitize-session-history.test-harness.ts rename to src/agents/embedded-agent-runner.sanitize-session-history.test-harness.ts index 93da203cdaa..ee1bd975d5e 100644 --- a/src/agents/pi-embedded-runner.sanitize-session-history.test-harness.ts +++ b/src/agents/embedded-agent-runner.sanitize-session-history.test-harness.ts @@ -1,6 +1,6 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { SessionManager } from "@earendil-works/pi-coding-agent"; import { expect, vi } from "vitest"; +import type { AgentMessage } from "./runtime/index.js"; +import type { SessionManager } from "./sessions/index.js"; import type { TranscriptPolicy } from "./transcript-policy.js"; type SessionEntry = { type: string; customType: string; data: unknown }; @@ -15,7 +15,7 @@ export type SanitizeSessionHistoryFn = (params: { policy?: TranscriptPolicy; preserveLatestAssistantThinking?: boolean; }) => Promise; -type SanitizeSessionHistoryMockedHelpers = typeof import("./pi-embedded-helpers.js"); +type SanitizeSessionHistoryMockedHelpers = typeof import("./embedded-agent-helpers.js"); export type SanitizeSessionHistoryHarness = { sanitizeSessionHistory: SanitizeSessionHistoryFn; mockedHelpers: SanitizeSessionHistoryMockedHelpers; @@ -63,7 +63,7 @@ export function makeSimpleUserMessages(): AgentMessage[] { export async function createSanitizeSessionHistoryHelpersMock(extra: Record = {}) { return { - ...(await vi.importActual("./pi-embedded-helpers.js")), + ...(await vi.importActual("./embedded-agent-helpers.js")), sanitizeSessionMessagesImages: vi.fn(async (msgs) => msgs), ...extra, }; @@ -107,9 +107,9 @@ export async function createSanitizeSessionHistoryProviderHookRuntimeMock( export async function loadSanitizeSessionHistoryWithCleanMocks(): Promise { vi.resetModules(); vi.resetAllMocks(); - const mockedHelpers = await import("./pi-embedded-helpers.js"); + const mockedHelpers = await import("./embedded-agent-helpers.js"); vi.mocked(mockedHelpers.sanitizeSessionMessagesImages).mockImplementation(async (msgs) => msgs); - const mod = await import("./pi-embedded-runner/replay-history.js"); + const mod = await import("./embedded-agent-runner/replay-history.js"); return { sanitizeSessionHistory: mod.sanitizeSessionHistory, mockedHelpers, diff --git a/src/agents/pi-embedded-runner.sanitize-session-history.test.ts b/src/agents/embedded-agent-runner.sanitize-session-history.test.ts similarity index 99% rename from src/agents/pi-embedded-runner.sanitize-session-history.test.ts rename to src/agents/embedded-agent-runner.sanitize-session-history.test.ts index 191acbcfdf2..e26f76ac69e 100644 --- a/src/agents/pi-embedded-runner.sanitize-session-history.test.ts +++ b/src/agents/embedded-agent-runner.sanitize-session-history.test.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, UserMessage, Usage } from "@earendil-works/pi-ai"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import type { AssistantMessage, UserMessage, Usage } from "openclaw/plugin-sdk/llm"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { expectOpenAIResponsesStrictSanitizeCall, @@ -14,16 +14,16 @@ import { type SanitizeSessionHistoryFn, sanitizeWithOpenAIResponses, TEST_SESSION_ID, -} from "./pi-embedded-runner.sanitize-session-history.test-harness.js"; -import { validateReplayTurns } from "./pi-embedded-runner/replay-history.js"; -import { OMITTED_ASSISTANT_REASONING_TEXT } from "./pi-embedded-runner/thinking.js"; +} from "./embedded-agent-runner.sanitize-session-history.test-harness.js"; +import { validateReplayTurns } from "./embedded-agent-runner/replay-history.js"; +import { OMITTED_ASSISTANT_REASONING_TEXT } from "./embedded-agent-runner/thinking.js"; import { castAgentMessage, castAgentMessages } from "./test-helpers/agent-message-fixtures.js"; import { extractToolCallsFromAssistant } from "./tool-call-id.js"; import type { TranscriptPolicy } from "./transcript-policy.js"; import { makeZeroUsageSnapshot } from "./usage.js"; -vi.mock("./pi-embedded-helpers.js", async () => ({ - ...(await vi.importActual("./pi-embedded-helpers.js")), +vi.mock("./embedded-agent-helpers.js", async () => ({ + ...(await vi.importActual("./embedded-agent-helpers.js")), isGoogleModelApi: vi.fn(), sanitizeSessionMessagesImages: vi.fn(async (msgs) => msgs), })); diff --git a/src/agents/pi-embedded-runner.splitsdktools.test.ts b/src/agents/embedded-agent-runner.splitsdktools.test.ts similarity index 82% rename from src/agents/pi-embedded-runner.splitsdktools.test.ts rename to src/agents/embedded-agent-runner.splitsdktools.test.ts index 084ddc872b7..70201657294 100644 --- a/src/agents/pi-embedded-runner.splitsdktools.test.ts +++ b/src/agents/embedded-agent-runner.splitsdktools.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest"; -import { splitSdkTools } from "./pi-embedded-runner.js"; +import { splitSdkTools } from "./embedded-agent-runner.js"; import { collectRegisteredToolNames, toSessionToolAllowlist, -} from "./pi-embedded-runner/tool-name-allowlist.js"; -import { createStubTool } from "./test-helpers/pi-tool-stubs.js"; +} from "./embedded-agent-runner/tool-name-allowlist.js"; +import { createStubTool } from "./test-helpers/agent-tool-stubs.js"; describe("splitSdkTools", () => { const tools = [ @@ -43,7 +43,7 @@ describe("splitSdkTools", () => { ]); }); - it("keeps OpenClaw-managed custom tools in Pi's session allowlist", () => { + it("keeps OpenClaw-managed custom tools in OpenClaw runtime's session allowlist", () => { const { customTools } = splitSdkTools({ tools: [createStubTool("read"), createStubTool("sessions_spawn")], sandboxEnabled: true, diff --git a/src/agents/embedded-agent-runner.ts b/src/agents/embedded-agent-runner.ts new file mode 100644 index 00000000000..07fd0fc2add --- /dev/null +++ b/src/agents/embedded-agent-runner.ts @@ -0,0 +1,25 @@ +export { compactEmbeddedAgentSession } from "./embedded-agent-runner/compact.queued.js"; +export { applyExtraParamsToAgent } from "./embedded-agent-runner/extra-params.js"; + +export { resolveEmbeddedSessionLane } from "./embedded-agent-runner/lanes.js"; +export { runEmbeddedAgent } from "./embedded-agent-runner/run.js"; +export { + abortAndDrainEmbeddedAgentRun, + abortEmbeddedAgentRun, + isEmbeddedAgentRunActive, + isEmbeddedAgentRunStreaming, + queueEmbeddedAgentMessage, + queueEmbeddedAgentMessageWithOutcome, + resolveActiveEmbeddedRunSessionId, + resolveActiveEmbeddedRunSessionId as resolveActiveEmbeddedAgentRunSessionId, + waitForEmbeddedAgentRunEnd, +} from "./embedded-agent-runner/runs.js"; +export { buildEmbeddedSandboxInfo } from "./embedded-agent-runner/sandbox-info.js"; +export { createSystemPromptOverride } from "./embedded-agent-runner/system-prompt.js"; +export { splitSdkTools } from "./embedded-agent-runner/tool-split.js"; +export type { + EmbeddedAgentMeta, + EmbeddedAgentCompactResult, + EmbeddedAgentRunMeta, + EmbeddedAgentRunResult, +} from "./embedded-agent-runner/types.js"; diff --git a/src/agents/pi-embedded-runner/abort.ts b/src/agents/embedded-agent-runner/abort.ts similarity index 100% rename from src/agents/pi-embedded-runner/abort.ts rename to src/agents/embedded-agent-runner/abort.ts diff --git a/src/agents/embedded-agent-runner/aliases.test.ts b/src/agents/embedded-agent-runner/aliases.test.ts new file mode 100644 index 00000000000..beff1863bff --- /dev/null +++ b/src/agents/embedded-agent-runner/aliases.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { normalizeEmbeddedAgentRuntime } from "../agent-runtime-id.js"; +import * as embeddedAgentRunner from "../embedded-agent-runner.js"; +import * as embeddedAgent from "../embedded-agent.js"; + +describe("embedded runner compatibility aliases", () => { + it("keeps the embedded-agent barrel bound to the runner implementation", () => { + expect(embeddedAgent.runEmbeddedAgent).toBe(embeddedAgentRunner.runEmbeddedAgent); + expect(embeddedAgent.compactEmbeddedAgentSession).toBe( + embeddedAgentRunner.compactEmbeddedAgentSession, + ); + expect(embeddedAgent.abortEmbeddedAgentRun).toBe(embeddedAgentRunner.abortEmbeddedAgentRun); + }); + + it("normalizes shipped Codex runtime aliases", () => { + expect(normalizeEmbeddedAgentRuntime("codex-app-server")).toBe("codex"); + }); + + it("normalizes legacy persisted runtime ids at plugin boundaries", () => { + expect(normalizeEmbeddedAgentRuntime("pi")).toBe("openclaw"); + }); + + it("does not rewrite custom runtime ids", () => { + expect(normalizeEmbeddedAgentRuntime("custom-harness")).toBe("custom-harness"); + }); +}); diff --git a/src/agents/pi-embedded-runner/cache-ttl.test.ts b/src/agents/embedded-agent-runner/cache-ttl.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/cache-ttl.test.ts rename to src/agents/embedded-agent-runner/cache-ttl.test.ts diff --git a/src/agents/pi-embedded-runner/cache-ttl.ts b/src/agents/embedded-agent-runner/cache-ttl.ts similarity index 97% rename from src/agents/pi-embedded-runner/cache-ttl.ts rename to src/agents/embedded-agent-runner/cache-ttl.ts index 85e02c08965..4885bb2fb8c 100644 --- a/src/agents/pi-embedded-runner/cache-ttl.ts +++ b/src/agents/embedded-agent-runner/cache-ttl.ts @@ -1,12 +1,12 @@ +import { + isAnthropicFamilyCacheTtlEligible, + isAnthropicModelRef, +} from "../../llm/providers/stream-wrappers/anthropic-family-cache-semantics.js"; import { resolveProviderCacheTtlEligibility } from "../../plugins/provider-runtime.js"; import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, } from "../../shared/string-coerce.js"; -import { - isAnthropicFamilyCacheTtlEligible, - isAnthropicModelRef, -} from "./anthropic-family-cache-semantics.js"; import { isGooglePromptCacheEligible } from "./prompt-cache-retention.js"; type CustomEntryLike = { type?: unknown; customType?: unknown; data?: unknown }; diff --git a/src/agents/pi-embedded-runner/compact-reasons.test.ts b/src/agents/embedded-agent-runner/compact-reasons.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/compact-reasons.test.ts rename to src/agents/embedded-agent-runner/compact-reasons.test.ts diff --git a/src/agents/pi-embedded-runner/compact-reasons.ts b/src/agents/embedded-agent-runner/compact-reasons.ts similarity index 100% rename from src/agents/pi-embedded-runner/compact-reasons.ts rename to src/agents/embedded-agent-runner/compact-reasons.ts diff --git a/src/agents/pi-embedded-runner/compact.hooks.harness.ts b/src/agents/embedded-agent-runner/compact.hooks.harness.ts similarity index 95% rename from src/agents/pi-embedded-runner/compact.hooks.harness.ts rename to src/agents/embedded-agent-runner/compact.hooks.harness.ts index b10c1dab859..790d0d6cc1f 100644 --- a/src/agents/pi-embedded-runner/compact.hooks.harness.ts +++ b/src/agents/embedded-agent-runner/compact.hooks.harness.ts @@ -78,7 +78,7 @@ export const resolveSessionAgentIdsMock = vi.fn(() => ({ sessionAgentId: "main", })); export const estimateTokensMock = vi.fn((_message?: unknown) => 10); -export const resolveAgentHarnessPolicyMock = vi.fn(() => ({ runtime: "pi" })); +export const resolveAgentHarnessPolicyMock = vi.fn(() => ({ runtime: "openclaw" })); export const resolveContextWindowInfoMock = vi.fn(() => ({ tokens: 128_000 })); function createDefaultSessionMessages(): unknown[] { return [ @@ -100,8 +100,8 @@ export const createOpenClawCodingToolsMock = vi.fn(() => []); export const guardSessionManagerMock = vi.fn(() => ({ flushPendingToolResults: vi.fn(), })); -export const applyPiCompactionSettingsFromConfigMock = vi.fn(); -export const createPreparedEmbeddedPiSettingsManagerMock = vi.fn(() => ({ +export const applyAgentCompactionSettingsFromConfigMock = vi.fn(); +export const createPreparedEmbeddedAgentSettingsManagerMock = vi.fn(() => ({ getGlobalSettings: vi.fn(() => ({})), })); export const listRegisteredPluginAgentPromptGuidanceMock = vi.fn((params?: { surface?: string }) => @@ -326,7 +326,7 @@ export function resetCompactHooksHarnessMocks(): void { modelRegistry: {}, }); resolveAgentHarnessPolicyMock.mockReset(); - resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "pi" }); + resolveAgentHarnessPolicyMock.mockReturnValue({ runtime: "openclaw" }); resolveContextWindowInfoMock.mockReset(); resolveContextWindowInfoMock.mockReturnValue({ tokens: 128_000 }); @@ -346,16 +346,16 @@ export function resetCompactHooksHarnessMocks(): void { guardSessionManagerMock.mockReturnValue({ flushPendingToolResults: vi.fn(), }); - applyPiCompactionSettingsFromConfigMock.mockReset(); - createPreparedEmbeddedPiSettingsManagerMock.mockReset(); - createPreparedEmbeddedPiSettingsManagerMock.mockReturnValue({ + applyAgentCompactionSettingsFromConfigMock.mockReset(); + createPreparedEmbeddedAgentSettingsManagerMock.mockReset(); + createPreparedEmbeddedAgentSettingsManagerMock.mockReturnValue({ getGlobalSettings: vi.fn(() => ({})), }); } export async function loadCompactHooksHarness(): Promise<{ - compactEmbeddedPiSessionDirect: typeof import("./compact.js").compactEmbeddedPiSessionDirect; - compactEmbeddedPiSession: typeof import("./compact.queued.js").compactEmbeddedPiSession; + compactEmbeddedAgentSessionDirect: typeof import("./compact.js").compactEmbeddedAgentSessionDirect; + compactEmbeddedAgentSession: typeof import("./compact.queued.js").compactEmbeddedAgentSession; testing: typeof import("./compact.js").testing; onSessionTranscriptUpdate: typeof import("../../sessions/transcript-events.js").onSessionTranscriptUpdate; }> { @@ -433,9 +433,9 @@ export async function loadCompactHooksHarness(): Promise<{ }; }); - vi.doMock("@earendil-works/pi-ai/oauth", async () => { - const actual = await vi.importActual( - "@earendil-works/pi-ai/oauth", + vi.doMock("@earendil../oauth.js", async () => { + const actual = await vi.importActual( + "@earendil../oauth.js", ); return { ...actual, @@ -444,7 +444,7 @@ export async function loadCompactHooksHarness(): Promise<{ }; }); - vi.doMock("@earendil-works/pi-coding-agent", () => ({ + vi.doMock("@earendil../sessions/index.js", () => ({ AuthStorage: function AuthStorage() {}, ModelRegistry: function ModelRegistry() {}, createAgentSession: vi.fn(async () => { @@ -492,10 +492,10 @@ export async function loadCompactHooksHarness(): Promise<{ guardSessionManager: guardSessionManagerMock, })); - vi.doMock("../pi-settings.js", () => ({ - applyPiAutoCompactionGuard: vi.fn(() => ({ supported: true, disabled: false })), - applyPiCompactionSettingsFromConfig: applyPiCompactionSettingsFromConfigMock, - ensurePiCompactionReserveTokens: vi.fn(), + vi.doMock("../agent-settings.js", () => ({ + applyAgentAutoCompactionGuard: vi.fn(() => ({ supported: true, disabled: false })), + applyAgentCompactionSettingsFromConfig: applyAgentCompactionSettingsFromConfigMock, + ensureAgentCompactionReserveTokens: vi.fn(), isSilentOverflowProneModel: vi.fn(() => false), resolveCompactionReserveTokensFloor: vi.fn(() => 0), })); @@ -569,7 +569,7 @@ export async function loadCompactHooksHarness(): Promise<{ resolveBootstrapContextForRun: vi.fn(async () => ({ contextFiles: [] })), })); - vi.doMock("../pi-bundle-mcp-tools.js", () => ({ + vi.doMock("../bundle-mcp-tools.js", () => ({ retireSessionMcpRuntime: vi.fn(async () => true), createBundleMcpToolRuntime: vi.fn(async () => ({ tools: [], @@ -577,7 +577,7 @@ export async function loadCompactHooksHarness(): Promise<{ })), })); - vi.doMock("../pi-bundle-lsp-runtime.js", () => ({ + vi.doMock("../bundle-lsp-runtime.js", () => ({ createBundleLspToolRuntime: vi.fn(async () => ({ tools: [], sessions: [], @@ -597,7 +597,7 @@ export async function loadCompactHooksHarness(): Promise<{ resolveChannelMessageToolHints: vi.fn(() => undefined), })); - vi.doMock("../pi-tools.js", () => ({ + vi.doMock("../agent-tools.js", () => ({ createOpenClawCodingTools: createOpenClawCodingToolsMock, resolveProcessToolScopeKey: ({ scopeKey, @@ -798,7 +798,7 @@ export async function loadCompactHooksHarness(): Promise<{ }; }); - vi.doMock("../pi-embedded-helpers.js", () => ({ + vi.doMock("../embedded-agent-helpers.js", () => ({ ensureSessionHeader: vi.fn(async () => {}), pickFallbackThinkingLevel: vi.fn((params: { message?: string; attempted?: Set }) => params.message?.includes("Reasoning is mandatory") && !params.attempted?.has("minimal") @@ -809,8 +809,8 @@ export async function loadCompactHooksHarness(): Promise<{ validateGeminiTurns: vi.fn((m: unknown[]) => m), })); - vi.doMock("../pi-project-settings.js", () => ({ - createPreparedEmbeddedPiSettingsManager: createPreparedEmbeddedPiSettingsManagerMock, + vi.doMock("../agent-project-settings.js", () => ({ + createPreparedEmbeddedAgentSettingsManager: createPreparedEmbeddedAgentSettingsManagerMock, })); vi.doMock("./sandbox-info.js", () => ({ @@ -855,7 +855,7 @@ export async function loadCompactHooksHarness(): Promise<{ return { ...compactModule, - compactEmbeddedPiSession: compactQueuedModule.compactEmbeddedPiSession, + compactEmbeddedAgentSession: compactQueuedModule.compactEmbeddedAgentSession, onSessionTranscriptUpdate: transcriptEvents.onSessionTranscriptUpdate, }; } diff --git a/src/agents/pi-embedded-runner/compact.hooks.test.ts b/src/agents/embedded-agent-runner/compact.hooks.test.ts similarity index 96% rename from src/agents/pi-embedded-runner/compact.hooks.test.ts rename to src/agents/embedded-agent-runner/compact.hooks.test.ts index 19e62646421..78a79484836 100644 --- a/src/agents/pi-embedded-runner/compact.hooks.test.ts +++ b/src/agents/embedded-agent-runner/compact.hooks.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { applyExtraParamsToAgentMock, @@ -35,8 +35,8 @@ import { triggerInternalHook, } from "./compact.hooks.harness.js"; -let compactEmbeddedPiSessionDirect: typeof import("./compact.js").compactEmbeddedPiSessionDirect; -let compactEmbeddedPiSession: typeof import("./compact.queued.js").compactEmbeddedPiSession; +let compactEmbeddedAgentSessionDirect: typeof import("./compact.js").compactEmbeddedAgentSessionDirect; +let compactEmbeddedAgentSession: typeof import("./compact.queued.js").compactEmbeddedAgentSession; let compactTesting: typeof import("./compact.js").testing; let onSessionTranscriptUpdate: typeof import("../../sessions/transcript-events.js").onSessionTranscriptUpdate; @@ -178,8 +178,8 @@ async function runCompactionHooks(params: { sessionKey?: string; messageProvider beforeAll(async () => { const loaded = await loadCompactHooksHarness(); - compactEmbeddedPiSessionDirect = loaded.compactEmbeddedPiSessionDirect; - compactEmbeddedPiSession = loaded.compactEmbeddedPiSession; + compactEmbeddedAgentSessionDirect = loaded.compactEmbeddedAgentSessionDirect; + compactEmbeddedAgentSession = loaded.compactEmbeddedAgentSession; compactTesting = loaded.testing; onSessionTranscriptUpdate = loaded.onSessionTranscriptUpdate; }); @@ -188,7 +188,7 @@ beforeEach(() => { resetCompactHooksHarnessMocks(); }); -describe("compactEmbeddedPiSessionDirect hooks", () => { +describe("compactEmbeddedAgentSessionDirect hooks", () => { beforeEach(() => { ensureRuntimePluginsLoaded.mockReset(); triggerInternalHook.mockClear(); @@ -216,7 +216,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { modelRegistry: {}, } as never); - await compactEmbeddedPiSessionDirect({ + await compactEmbeddedAgentSessionDirect({ sessionId: "session-1", sessionFile: "/tmp/session.jsonl", workspaceDir: "/tmp/workspace", @@ -238,7 +238,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { modelRegistry: {}, } as never); - await compactEmbeddedPiSessionDirect({ + await compactEmbeddedAgentSessionDirect({ sessionId: "session-1", sessionFile: "/tmp/session.jsonl", workspaceDir: "/tmp/workspace", @@ -253,7 +253,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { }); it("uses sandboxSessionKey only for compaction sandbox resolution", async () => { - await compactEmbeddedPiSessionDirect({ + await compactEmbeddedAgentSessionDirect({ sessionId: "session-1", sessionKey: "agent:main:main", sandboxSessionKey: "agent:main:telegram:default:direct:12345", @@ -269,7 +269,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { }); it("uses subagent prompt surface and guidance for compacted subagent prompt rebuilds", async () => { - await compactEmbeddedPiSessionDirect({ + await compactEmbeddedAgentSessionDirect({ sessionId: "session-1", sessionKey: "agent:main:subagent:worker", sessionFile: "/tmp/session.jsonl", @@ -289,7 +289,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { }); it("uses ACP prompt surface and guidance for compacted ACP prompt rebuilds", async () => { - await compactEmbeddedPiSessionDirect({ + await compactEmbeddedAgentSessionDirect({ sessionId: "session-1", sessionKey: "agent:codex:acp:worker", sessionFile: "/tmp/session.jsonl", @@ -367,7 +367,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { }); it("preserves full sender identity when building compaction tools", async () => { - await compactEmbeddedPiSessionDirect({ + await compactEmbeddedAgentSessionDirect({ sessionId: "session-1", sessionFile: "/tmp/session.jsonl", workspaceDir: "/tmp/workspace", @@ -443,7 +443,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { details: { ok: true }, }); - const result = await compactEmbeddedPiSessionDirect({ + const result = await compactEmbeddedAgentSessionDirect({ sessionId: "session-1", sessionKey: TEST_SESSION_KEY, sessionFile: "/tmp/session.jsonl", @@ -694,7 +694,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { }; const configBefore = structuredClone(config); - const result = await compactEmbeddedPiSessionDirect({ + const result = await compactEmbeddedAgentSessionDirect({ sessionId: "session-1", sessionKey: TEST_SESSION_KEY, sessionFile: "/tmp/session.jsonl", @@ -736,7 +736,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { Object.assign(new Error("400 invalid request body"), { status: 400 }), ); - const result = await compactEmbeddedPiSessionDirect({ + const result = await compactEmbeddedAgentSessionDirect({ sessionId: "session-1", sessionKey: TEST_SESSION_KEY, sessionFile: "/tmp/session.jsonl", @@ -782,7 +782,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { }), ); - const result = await compactEmbeddedPiSessionDirect({ + const result = await compactEmbeddedAgentSessionDirect({ sessionId: "session-1", sessionKey: TEST_SESSION_KEY, sessionFile: "/tmp/session.jsonl", @@ -990,7 +990,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { }); try { - const result = await compactEmbeddedPiSessionDirect({ + const result = await compactEmbeddedAgentSessionDirect({ sessionId: "session-1", sessionKey: TEST_SESSION_KEY, sessionFile: "/tmp/session.jsonl", @@ -1336,7 +1336,7 @@ describe("compactEmbeddedPiSessionDirect hooks", () => { }); }); -describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { +describe("compactEmbeddedAgentSession hooks (ownsCompaction engine)", () => { beforeEach(() => { hookRunner.hasHooks.mockReset(); hookRunner.runBeforeCompaction.mockReset(); @@ -1362,7 +1362,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { sessionAgentId: "lossless-agent", }); - await compactEmbeddedPiSession( + await compactEmbeddedAgentSession( wrappedCompactionArgs({ config: { agents: { @@ -1406,7 +1406,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { it("fires before_compaction with sentinel -1 and after_compaction on success", async () => { hookRunner.hasHooks.mockReturnValue(true); - const result = await compactEmbeddedPiSession( + const result = await compactEmbeddedAgentSession( wrappedCompactionArgs({ messageChannel: "telegram", }), @@ -1453,7 +1453,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { }, } as never); - const result = await compactEmbeddedPiSession(wrappedCompactionArgs()); + const result = await compactEmbeddedAgentSession(wrappedCompactionArgs()); expect(result.ok).toBe(true); expectRecordFields(mockCallArg(hookRunner.runAfterCompaction), { @@ -1472,7 +1472,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { getMemorySearchManagerMock.mockResolvedValue({ manager: { sync } }); try { - const result = await compactEmbeddedPiSession( + const result = await compactEmbeddedAgentSession( wrappedCompactionArgs({ sessionFile: ` ${TEST_SESSION_FILE} `, config: compactionConfig("await"), @@ -1506,7 +1506,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { maintain, } as never); - const result = await compactEmbeddedPiSession(wrappedCompactionArgs()); + const result = await compactEmbeddedAgentSession(wrappedCompactionArgs()); expect(result.ok).toBe(true); const runtimeContext = ( @@ -1521,7 +1521,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { }); it("resolves the effective compaction model before manual engine-owned compaction", async () => { - await compactEmbeddedPiSession( + await compactEmbeddedAgentSession( wrappedCompactionArgs({ config: { agents: { @@ -1596,7 +1596,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { }, }); - const result = await compactEmbeddedPiSession( + const result = await compactEmbeddedAgentSession( wrappedCompactionArgs({ provider: "openai-codex", model: "gpt-5.4", @@ -1662,7 +1662,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { failure: { reason: "missing_thread_binding" }, }); - const result = await compactEmbeddedPiSession( + const result = await compactEmbeddedAgentSession( wrappedCompactionArgs({ provider: "openai-codex", model: "gpt-5.4", @@ -1689,7 +1689,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { result: undefined, }); - const result = await compactEmbeddedPiSession(wrappedCompactionArgs()); + const result = await compactEmbeddedAgentSession(wrappedCompactionArgs()); expect(result.ok).toBe(false); expect(hookRunner.runBeforeCompaction).toHaveBeenCalledTimes(1); @@ -1704,7 +1704,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { // instead of throwing a raw rejection at callers that only read result.ok. contextEngineCompactMock.mockRejectedValue(new Error("Compaction timed out after 900000ms")); - const result = await compactEmbeddedPiSession(wrappedCompactionArgs()); + const result = await compactEmbeddedAgentSession(wrappedCompactionArgs()); expect(result.ok).toBe(false); expect(result.compacted).toBe(false); @@ -1715,7 +1715,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { it("threads the caller abort signal into the engine compact() call", async () => { const controller = new AbortController(); - const result = await compactEmbeddedPiSession( + const result = await compactEmbeddedAgentSession( wrappedCompactionArgs({ abortSignal: controller.signal }), ); @@ -1735,7 +1735,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { }); try { - const result = await compactEmbeddedPiSession( + const result = await compactEmbeddedAgentSession( wrappedCompactionArgs({ config: compactionConfig("await"), }), @@ -1776,7 +1776,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { }, } as never); - const result = await compactEmbeddedPiSession( + const result = await compactEmbeddedAgentSession( wrappedCompactionArgs({ config: { agents: { @@ -1823,7 +1823,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { sessionFile: TEST_SESSION_FILE, }, } as never); - const result = await compactEmbeddedPiSession( + const result = await compactEmbeddedAgentSession( wrappedCompactionArgs({ config: { agents: { @@ -1851,7 +1851,7 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => { hookRunner.hasHooks.mockReturnValue(true); hookRunner.runBeforeCompaction.mockRejectedValue(new Error("hook boom")); - const result = await compactEmbeddedPiSession(wrappedCompactionArgs()); + const result = await compactEmbeddedAgentSession(wrappedCompactionArgs()); expect(result.ok).toBe(true); expect(result.compacted).toBe(true); diff --git a/src/agents/pi-embedded-runner/compact.queued.ts b/src/agents/embedded-agent-runner/compact.queued.ts similarity index 93% rename from src/agents/pi-embedded-runner/compact.queued.ts rename to src/agents/embedded-agent-runner/compact.queued.ts index 16a45416ace..bfa347d15e0 100644 --- a/src/agents/pi-embedded-runner/compact.queued.ts +++ b/src/agents/embedded-agent-runner/compact.queued.ts @@ -27,7 +27,7 @@ import { import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js"; import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js"; import { DEFERRED_CONTEXT_ENGINE_COMPACTION_REASON } from "./compact-reasons.js"; -import type { CompactEmbeddedPiSessionParams } from "./compact.types.js"; +import type { CompactEmbeddedAgentSessionParams } from "./compact.types.js"; import { asCompactionHookRunner, runPostCompactionSideEffects } from "./compaction-hooks.js"; import { buildEmbeddedCompactionRuntimeContext, @@ -45,19 +45,14 @@ import { resolveContextEngineCapabilities } from "./context-engine-capabilities. import { runContextEngineMaintenance } from "./context-engine-maintenance.js"; import { resolveGlobalLane, resolveSessionLane } from "./lanes.js"; import { log } from "./logger.js"; -import { readPiModelContextTokens } from "./model-context-tokens.js"; +import { readAgentModelContextTokens } from "./model-context-tokens.js"; import { resolveModelAsync } from "./model.js"; -import type { EmbeddedPiCompactResult } from "./types.js"; +import type { EmbeddedAgentCompactResult } from "./types.js"; import { normalizeContextTokenBudget } from "./utils.js"; function shouldFallbackAfterHarnessCompaction( - result: EmbeddedPiCompactResult | undefined, - harnessPolicyRuntime: string | undefined, - explicitHarnessId: string | undefined, + result: EmbeddedAgentCompactResult | undefined, ): boolean { - if (harnessPolicyRuntime === "codex" || explicitHarnessId === "codex") { - return false; - } return ( result?.ok === false && (result.failure?.reason === "missing_thread_binding" || @@ -69,7 +64,7 @@ const DEFERRED_CONTEXT_ENGINE_COMPACTION_SCHEDULE_FAILURE_REASON = "failed to schedule background context-engine maintenance"; function shouldDeferOwningContextEngineBudgetCompaction(params: { - compactParams: CompactEmbeddedPiSessionParams; + compactParams: CompactEmbeddedAgentSessionParams; contextEngine: ContextEngine; }): boolean { // Request-time budget compaction for context-engine-owned transcripts can @@ -96,7 +91,7 @@ async function disposeContextEngine(contextEngine: ContextEngine): Promise } async function deferOwningContextEngineBudgetCompaction(params: { - compactParams: CompactEmbeddedPiSessionParams; + compactParams: CompactEmbeddedAgentSessionParams; contextEngine: ContextEngine; contextEngineRuntimeContext: ContextEngineRuntimeContext; }): Promise { @@ -155,11 +150,11 @@ async function deferOwningContextEngineBudgetCompaction(params: { /** * Compacts a session with lane queueing (session lane + global lane). * Use this from outside a lane context. If already inside a lane, use - * `compactEmbeddedPiSessionDirect` to avoid deadlocks. + * `compactEmbeddedAgentSessionDirect` to avoid deadlocks. */ -export async function compactEmbeddedPiSession( - params: CompactEmbeddedPiSessionParams, -): Promise { +export async function compactEmbeddedAgentSession( + params: CompactEmbeddedAgentSessionParams, +): Promise { ensureRuntimePluginsLoaded({ config: params.config, workspaceDir: params.workspaceDir, @@ -207,10 +202,10 @@ export async function compactEmbeddedPiSession( cfg: params.config, provider: resolveContextConfigProviderForRuntime({ provider: ceProvider, - runtimeId: ceHarnessPolicy.runtime, + runtimeId: params.agentHarnessId ?? ceHarnessPolicy.runtime, }), modelId: ceModelId, - modelContextTokens: readPiModelContextTokens(ceModel), + modelContextTokens: readAgentModelContextTokens(ceModel), modelContextWindow: ceRuntimeModel?.contextWindow, defaultTokens: DEFAULT_CONTEXT_TOKENS, }).tokens, @@ -226,13 +221,6 @@ export async function compactEmbeddedPiSession( contextTokenBudget, contextEnginePluginId: resolveContextEngineOwnerPluginId(contextEngine), }); - const harnessPolicy = resolveAgentHarnessPolicy({ - provider: params.provider, - modelId: params.model, - config: params.config, - agentId: agentIds.sessionAgentId, - sessionKey: params.sessionKey, - }); const harnessResult = await maybeCompactAgentHarnessSession({ ...params, contextEngine, @@ -240,13 +228,7 @@ export async function compactEmbeddedPiSession( contextEngineRuntimeContext, }); if (harnessResult) { - if ( - !shouldFallbackAfterHarnessCompaction( - harnessResult, - harnessPolicy.runtime, - params.agentHarnessId, - ) - ) { + if (!shouldFallbackAfterHarnessCompaction(harnessResult)) { await contextEngine.dispose?.(); return harnessResult; } @@ -276,7 +258,7 @@ export async function compactEmbeddedPiSession( let checkpointSnapshotRetained = false; try { // When the context engine owns compaction, its compact() implementation - // bypasses compactEmbeddedPiSessionDirect (which fires the hooks internally). + // bypasses compactEmbeddedAgentSessionDirect (which fires the hooks internally). // Fire before_compaction / after_compaction hooks here so plugin subscribers // are notified regardless of which engine is active. const engineOwnsCompaction = contextEngine.info.ownsCompaction === true; @@ -487,7 +469,7 @@ export async function compactEmbeddedPiSession( } function buildCompactionContextEngineRuntimeContext(params: { - params: CompactEmbeddedPiSessionParams; + params: CompactEmbeddedAgentSessionParams; agentDir: string; contextEnginePluginId?: string; contextTokenBudget?: number; diff --git a/src/agents/embedded-agent-runner/compact.runtime.ts b/src/agents/embedded-agent-runner/compact.runtime.ts new file mode 100644 index 00000000000..8a1fb14fd6e --- /dev/null +++ b/src/agents/embedded-agent-runner/compact.runtime.ts @@ -0,0 +1,15 @@ +import { createLazyImportLoader } from "../../shared/lazy-promise.js"; +import type { CompactEmbeddedAgentSessionDirect } from "./compact.runtime.types.js"; + +const compactRuntimeLoader = createLazyImportLoader(() => import("./compact.js")); + +function loadCompactRuntime() { + return compactRuntimeLoader.load(); +} + +export async function compactEmbeddedAgentSessionDirect( + ...args: Parameters +): ReturnType { + const { compactEmbeddedAgentSessionDirect } = await loadCompactRuntime(); + return compactEmbeddedAgentSessionDirect(...args); +} diff --git a/src/agents/embedded-agent-runner/compact.runtime.types.ts b/src/agents/embedded-agent-runner/compact.runtime.types.ts new file mode 100644 index 00000000000..07824d6863a --- /dev/null +++ b/src/agents/embedded-agent-runner/compact.runtime.types.ts @@ -0,0 +1,6 @@ +import type { CompactEmbeddedAgentSessionParams } from "./compact.types.js"; +import type { EmbeddedAgentCompactResult } from "./types.js"; + +export type CompactEmbeddedAgentSessionDirect = ( + params: CompactEmbeddedAgentSessionParams, +) => Promise; diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/embedded-agent-runner/compact.ts similarity index 94% rename from src/agents/pi-embedded-runner/compact.ts rename to src/agents/embedded-agent-runner/compact.ts index fe244b3262b..ae7635b66d1 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/embedded-agent-runner/compact.ts @@ -1,11 +1,5 @@ import fs from "node:fs/promises"; import os from "node:os"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { - createAgentSession, - estimateTokens, - SessionManager, -} from "@earendil-works/pi-coding-agent"; import { isAcpRuntimeSpawnAvailable } from "../../acp/runtime/availability.js"; import type { ThinkLevel } from "../../auto-reply/thinking.js"; import { resolveAgentModelFallbackValues } from "../../config/model-input.js"; @@ -34,11 +28,24 @@ import { isCronSessionKey, isSubagentSessionKey } from "../../routing/session-ke import { resolveUserPath } from "../../utils.js"; import { normalizeMessageChannel } from "../../utils/message-channel.js"; import { isReasoningTagProvider } from "../../utils/provider-utils.js"; +import { createBundleLspToolRuntime } from "../agent-bundle-lsp-runtime.js"; +import { createBundleMcpToolRuntime } from "../agent-bundle-mcp-tools.js"; +import { + consumeCompactionSafeguardCancelReason, + setCompactionSafeguardCancelReason, +} from "../agent-hooks/compaction-safeguard-runtime.js"; +import { createPreparedEmbeddedAgentSettingsManager } from "../agent-project-settings.js"; import { resolveAgentDir, resolveRunModelFallbacksOverride, resolveSessionAgentIds, } from "../agent-scope.js"; +import { + applyAgentAutoCompactionGuard, + applyAgentCompactionSettingsFromConfig, + isSilentOverflowProneModel, +} from "../agent-settings.js"; +import { createOpenClawCodingTools, resolveProcessToolScopeKey } from "../agent-tools.js"; import { listActiveProcessSessionReferences } from "../bash-process-references.js"; import { makeBootstrapWarn, @@ -58,6 +65,8 @@ import { resolveContextWindowInfo } from "../context-window-guard.js"; import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js"; import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js"; import { resolveOpenClawReferencePaths } from "../docs-path.js"; +import { ensureSessionHeader } from "../embedded-agent-helpers.js"; +import { pickFallbackThinkingLevel } from "../embedded-agent-helpers.js"; import { coerceToFailoverError, describeFailoverError } from "../failover-error.js"; import { ensureSelectedAgentHarnessPlugin } from "../harness/runtime-plugin.js"; import { resolveAgentHarnessPolicy } from "../harness/selection.js"; @@ -72,25 +81,7 @@ import { import { isFallbackSummaryError, runWithModelFallback } from "../model-fallback.js"; import { supportsModelTools } from "../model-tool-support.js"; import { ensureOpenClawModelsJson } from "../models-config.js"; -import { - resolveContextConfigProviderForRuntime, - resolveOpenAICompactionRuntimeProvider, -} from "../openai-codex-routing.js"; -import { createBundleLspToolRuntime } from "../pi-bundle-lsp-runtime.js"; -import { createBundleMcpToolRuntime } from "../pi-bundle-mcp-tools.js"; -import { ensureSessionHeader } from "../pi-embedded-helpers.js"; -import { pickFallbackThinkingLevel } from "../pi-embedded-helpers.js"; -import { - consumeCompactionSafeguardCancelReason, - setCompactionSafeguardCancelReason, -} from "../pi-hooks/compaction-safeguard-runtime.js"; -import { createPreparedEmbeddedPiSettingsManager } from "../pi-project-settings.js"; -import { - applyPiAutoCompactionGuard, - applyPiCompactionSettingsFromConfig, - isSilentOverflowProneModel, -} from "../pi-settings.js"; -import { createOpenClawCodingTools, resolveProcessToolScopeKey } from "../pi-tools.js"; +import { resolveContextConfigProviderForRuntime } from "../openai-codex-routing.js"; import { wrapStreamFnTextTransforms } from "../plugin-text-transforms.js"; import { resolveAgentPromptSurfaceForSessionKey } from "../prompt-surface.js"; import { registerProviderStreamForModel } from "../provider-stream.js"; @@ -98,6 +89,7 @@ import { collectRuntimeChannelCapabilities } from "../runtime-capabilities.js"; import { buildAgentRuntimePlan } from "../runtime-plan/build.js"; import type { AgentRuntimePlan } from "../runtime-plan/types.js"; import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js"; +import type { AgentMessage } from "../runtime/index.js"; import { resolveSandboxContext } from "../sandbox.js"; import { repairSessionFileIfNeeded } from "../session-file-repair.js"; import { guardSessionManager } from "../session-tool-result-guard-wrapper.js"; @@ -107,6 +99,7 @@ import { resolveSessionLockMaxHoldFromTimeout, resolveSessionWriteLockOptions, } from "../session-write-lock.js"; +import { createAgentSession, estimateTokens, SessionManager } from "../sessions/index.js"; import { detectRuntimeShell } from "../shell-utils.js"; import { applySkillEnvOverrides, @@ -119,7 +112,10 @@ import { formatUnknownCompactionReasonDetail, resolveCompactionFailureReason, } from "./compact-reasons.js"; -import type { CompactEmbeddedPiSessionParams, CompactionMessageMetrics } from "./compact.types.js"; +import type { + CompactEmbeddedAgentSessionParams, + CompactionMessageMetrics, +} from "./compact.types.js"; import { dedupeDuplicateUserMessagesForCompaction } from "./compaction-duplicate-user-messages.js"; import { asCompactionHookRunner, @@ -146,10 +142,10 @@ import { getHistoryLimitFromSessionKey, limitHistoryTurns } from "./history.js"; import { log } from "./logger.js"; import { hardenManualCompactionBoundary } from "./manual-compaction-boundary.js"; import { buildEmbeddedMessageActionDiscoveryInput } from "./message-action-discovery-input.js"; -import { readPiModelContextTokens } from "./model-context-tokens.js"; +import { readAgentModelContextTokens } from "./model-context-tokens.js"; import { resolveModelAsync } from "./model.js"; import { sanitizeSessionHistory, validateReplayTurns } from "./replay-history.js"; -import { createEmbeddedPiResourceLoader } from "./resource-loader.js"; +import { createEmbeddedAgentResourceLoader } from "./resource-loader.js"; import { buildEmbeddedSandboxInfo } from "./sandbox-info.js"; import { prewarmSessionFile, trackSessionManagerAccess } from "./session-manager-cache.js"; import { resolveEmbeddedRunSkillEntries } from "./skills-runtime.js"; @@ -169,10 +165,10 @@ import { } from "./tool-name-allowlist.js"; import { splitSdkTools } from "./tool-split.js"; import { readTranscriptFileState } from "./transcript-file-state.js"; -import type { EmbeddedPiCompactResult } from "./types.js"; +import type { EmbeddedAgentCompactResult } from "./types.js"; import { mapThinkingLevel, normalizeContextTokenBudget } from "./utils.js"; import { flushPendingToolResultsAfterIdle } from "./wait-for-idle-before-flush.js"; -export type { CompactEmbeddedPiSessionParams } from "./compact.types.js"; +export type { CompactEmbeddedAgentSessionParams } from "./compact.types.js"; function hasRealConversationContent( msg: AgentMessage, @@ -360,12 +356,12 @@ function containsRealConversationMessages(messages: AgentMessage[]): boolean { ); } -function hasExplicitCompactionModel(params: CompactEmbeddedPiSessionParams): boolean { +function hasExplicitCompactionModel(params: CompactEmbeddedAgentSessionParams): boolean { return Boolean(params.config?.agents?.defaults?.compaction?.model?.trim()); } function resolveCompactionFallbacksOverride( - params: CompactEmbeddedPiSessionParams, + params: CompactEmbeddedAgentSessionParams, ): string[] | undefined { return ( params.modelFallbacksOverride ?? @@ -376,14 +372,14 @@ function resolveCompactionFallbacksOverride( ); } -function hasCompactionModelFallbackCandidates(params: CompactEmbeddedPiSessionParams): boolean { +function hasCompactionModelFallbackCandidates(params: CompactEmbeddedAgentSessionParams): boolean { const fallbacksOverride = resolveCompactionFallbacksOverride(params); const defaultFallbacks = resolveAgentModelFallbackValues(params.config?.agents?.defaults?.model); return (fallbacksOverride ?? defaultFallbacks).length > 0; } function classifyCompactionFallbackResult( - result: EmbeddedPiCompactResult, + result: EmbeddedAgentCompactResult, provider: string, model: string, ) { @@ -402,7 +398,7 @@ function classifyCompactionFallbackResult( return failoverError ? { error: failoverError } : null; } -function fallbackFailureToCompactionResult(err: unknown): EmbeddedPiCompactResult { +function fallbackFailureToCompactionResult(err: unknown): EmbeddedAgentCompactResult { const reason = isFallbackSummaryError(err) ? err.message : formatErrorMessage(err); return { ok: false, @@ -415,11 +411,11 @@ function fallbackFailureToCompactionResult(err: unknown): EmbeddedPiCompactResul * Core compaction logic without lane queueing. * Use this when already inside a session/global lane to avoid deadlocks. */ -export async function compactEmbeddedPiSessionDirect( - params: CompactEmbeddedPiSessionParams, -): Promise { +export async function compactEmbeddedAgentSessionDirect( + params: CompactEmbeddedAgentSessionParams, +): Promise { if (hasExplicitCompactionModel(params) || !hasCompactionModelFallbackCandidates(params)) { - return await compactEmbeddedPiSessionDirectOnce(params); + return await compactEmbeddedAgentSessionDirectOnce(params); } const resolvedCompactionTarget = resolveEmbeddedCompactionTarget({ config: params.config, @@ -439,7 +435,7 @@ export async function compactEmbeddedPiSessionDirect( }).sessionAgentId; const fallbackSessionKey = params.sandboxSessionKey ?? params.sessionKey ?? params.sessionId; try { - const fallbackResult = await runWithModelFallback({ + const fallbackResult = await runWithModelFallback({ cfg: params.config, provider: primaryProvider, model: primaryModel, @@ -465,7 +461,7 @@ export async function compactEmbeddedPiSessionDirect( const preservesPrimaryAuth = provider === primaryProvider || provider === requestedPrimaryProvider; const authProfileId = preservesPrimaryAuth ? params.authProfileId : undefined; - return await compactEmbeddedPiSessionDirectOnce({ + return await compactEmbeddedAgentSessionDirectOnce({ ...params, provider, model, @@ -479,9 +475,9 @@ export async function compactEmbeddedPiSessionDirect( } } -async function compactEmbeddedPiSessionDirectOnce( - params: CompactEmbeddedPiSessionParams, -): Promise { +async function compactEmbeddedAgentSessionDirectOnce( + params: CompactEmbeddedAgentSessionParams, +): Promise { const startedAt = Date.now(); const diagId = params.diagId?.trim() || createCompactionDiagId(); const trigger = params.trigger ?? "manual"; @@ -504,32 +500,12 @@ async function compactEmbeddedPiSessionDirectOnce( }); // Keep the configured provider for harness policy, while auth/model loading below can // route OpenAI compaction through Codex OAuth when that runtime owns the session credentials. - const modelConfigProvider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER; + const provider = resolvedCompactionTarget.provider ?? DEFAULT_PROVIDER; const modelId = resolvedCompactionTarget.model ?? DEFAULT_MODEL; const authProfileId = resolvedCompactionTarget.authProfileId; - const earlyAgentIds = resolveSessionAgentIds({ - sessionKey: params.sessionKey, - config: params.config, - }); - const runtimeHarnessPolicy = resolveAgentHarnessPolicy({ - provider: modelConfigProvider, - modelId, - config: params.config, - agentId: earlyAgentIds.sessionAgentId, - sessionKey: params.sessionKey, - }); - const selectedHarnessRuntime = params.agentHarnessId ?? runtimeHarnessPolicy.runtime; - const provider = resolveOpenAICompactionRuntimeProvider({ - provider: modelConfigProvider, - harnessRuntime: runtimeHarnessPolicy.runtime, - agentHarnessId: params.agentHarnessId, - authProfileId, - config: params.config, - workspaceDir: resolvedWorkspace, - }); let thinkLevel: ThinkLevel = params.thinkLevel ?? "off"; const attemptedThinking = new Set(); - const fail = (reason: string, err?: unknown): EmbeddedPiCompactResult => { + const fail = (reason: string, err?: unknown): EmbeddedAgentCompactResult => { const failureReason = classifyCompactionReason(reason); const failure = err ? describeFailoverError(err) : undefined; const detail = @@ -555,6 +531,10 @@ async function compactEmbeddedPiSessionDirectOnce( : undefined, }; }; + const earlyAgentIds = resolveSessionAgentIds({ + sessionKey: params.sessionKey, + config: params.config, + }); const agentDir = params.agentDir ?? resolveAgentDir(params.config ?? {}, earlyAgentIds.sessionAgentId); await ensureOpenClawModelsJson(params.config, agentDir, { @@ -639,7 +619,10 @@ async function compactEmbeddedPiSessionDirectOnce( sessionId: params.sessionId, cwd: effectiveWorkspace, }); - const effectiveSkillAgentId = earlyAgentIds.sessionAgentId; + const { sessionAgentId: effectiveSkillAgentId } = resolveSessionAgentIds({ + sessionKey: params.sessionKey, + config: params.config, + }); let restoreSkillEnv: (() => void) | undefined; let compactionSessionManager: unknown = null; @@ -688,17 +671,24 @@ async function compactEmbeddedPiSessionDirectOnce( warn: (message) => log.warn(message), }), }); - // Apply contextTokens cap to model so pi-coding-agent's auto-compaction + // Apply contextTokens cap to model so session runtime's auto-compaction // threshold uses the effective limit, not the native context window. const runtimeModelWithContext = runtimeModel as ProviderRuntimeModel; + const runtimeHarnessPolicy = resolveAgentHarnessPolicy({ + provider, + modelId, + config: params.config, + agentId: effectiveSkillAgentId, + sessionKey: params.sessionKey, + }); const ctxInfo = resolveContextWindowInfo({ cfg: params.config, provider: resolveContextConfigProviderForRuntime({ - provider: modelConfigProvider, - runtimeId: selectedHarnessRuntime, + provider, + runtimeId: runtimeHarnessPolicy.runtime, }), modelId, - modelContextTokens: readPiModelContextTokens(runtimeModel), + modelContextTokens: readAgentModelContextTokens(runtimeModel), modelContextWindow: runtimeModelWithContext.contextWindow, defaultTokens: DEFAULT_CONTEXT_TOKENS, }); @@ -1042,7 +1032,7 @@ async function compactEmbeddedPiSessionDirectOnce( }); compactionSessionManager = sessionManager; trackSessionManagerAccess(params.sessionFile); - const settingsManager = createPreparedEmbeddedPiSettingsManager({ + const settingsManager = createPreparedEmbeddedAgentSettingsManager({ cwd: effectiveWorkspace, agentDir, cfg: params.config, @@ -1058,12 +1048,11 @@ async function compactEmbeddedPiSessionDirectOnce( const extensionFactories = buildEmbeddedExtensionFactories({ cfg: params.config, sessionManager, - workspaceDir: effectiveWorkspace, provider, modelId, model, }); - const resourceLoader = createEmbeddedPiResourceLoader({ + const resourceLoader = createEmbeddedAgentResourceLoader({ cwd: resolvedWorkspace, agentDir, settingsManager, @@ -1071,11 +1060,11 @@ async function compactEmbeddedPiSessionDirectOnce( }); await resourceLoader.reload(); // DefaultResourceLoader.reload() rehydrates settings from disk and can drop OpenClaw - // compaction overrides applied in createPreparedEmbeddedPiSettingsManager — same - // rehydration also restores Pi's auto-compaction (openclaw#75799), so re-apply + // compaction overrides applied in createPreparedEmbeddedAgentSettingsManager — same + // rehydration also restores OpenClaw runtime's auto-compaction (openclaw#75799), so re-apply // both guards. effectiveModel.baseUrl matches the surrounding scope so // auth-profile-injected baseUrls reach the endpoint-class detector. - applyPiCompactionSettingsFromConfig({ + applyAgentCompactionSettingsFromConfig({ settingsManager, cfg: params.config, contextTokenBudget, @@ -1083,7 +1072,7 @@ async function compactEmbeddedPiSessionDirectOnce( // contextEngineInfo is intentionally omitted: this guard runs inside the // compaction LLM session, which is not the user-facing agent session and // has no associated context engine. - applyPiAutoCompactionGuard({ + applyAgentAutoCompactionGuard({ settingsManager, silentOverflowProneProvider: isSilentOverflowProneModel({ provider, @@ -1105,7 +1094,7 @@ async function compactEmbeddedPiSessionDirectOnce( channelId: params.currentChannelId, }, }); - // Pi treats `tools` as a name allowlist during session creation. Pass the + // The session runtime treats `tools` as a name allowlist during session creation. Pass the // exact OpenClaw-managed registrations so custom tools survive startup. const sessionToolAllowlist = toSessionToolAllowlist(collectRegisteredToolNames(customTools)); diff --git a/src/agents/pi-embedded-runner/compact.types.ts b/src/agents/embedded-agent-runner/compact.types.ts similarity index 98% rename from src/agents/pi-embedded-runner/compact.types.ts rename to src/agents/embedded-agent-runner/compact.types.ts index add0e8fa7f7..605b0eea2d3 100644 --- a/src/agents/pi-embedded-runner/compact.types.ts +++ b/src/agents/embedded-agent-runner/compact.types.ts @@ -7,7 +7,7 @@ import type { ExecElevatedDefaults } from "../bash-tools.exec-types.js"; import type { AgentRuntimePlan } from "../runtime-plan/types.js"; import type { SkillSnapshot } from "../skills.js"; -export type CompactEmbeddedPiSessionParams = { +export type CompactEmbeddedAgentSessionParams = { sessionId: string; runId?: string; sessionKey?: string; diff --git a/src/agents/pi-embedded-runner/compaction-duplicate-user-messages.test.ts b/src/agents/embedded-agent-runner/compaction-duplicate-user-messages.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/compaction-duplicate-user-messages.test.ts rename to src/agents/embedded-agent-runner/compaction-duplicate-user-messages.test.ts diff --git a/src/agents/pi-embedded-runner/compaction-duplicate-user-messages.ts b/src/agents/embedded-agent-runner/compaction-duplicate-user-messages.ts similarity index 100% rename from src/agents/pi-embedded-runner/compaction-duplicate-user-messages.ts rename to src/agents/embedded-agent-runner/compaction-duplicate-user-messages.ts diff --git a/src/agents/pi-embedded-runner/compaction-hooks.ts b/src/agents/embedded-agent-runner/compaction-hooks.ts similarity index 99% rename from src/agents/pi-embedded-runner/compaction-hooks.ts rename to src/agents/embedded-agent-runner/compaction-hooks.ts index 8efdd666456..25ac76cdbbe 100644 --- a/src/agents/pi-embedded-runner/compaction-hooks.ts +++ b/src/agents/embedded-agent-runner/compaction-hooks.ts @@ -1,4 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js"; import { formatErrorMessage } from "../../infra/errors.js"; @@ -7,6 +6,7 @@ import { getActiveMemorySearchManager } from "../../plugins/memory-runtime.js"; import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js"; import { resolveSessionAgentId } from "../agent-scope.js"; import { resolveMemorySearchConfig } from "../memory-search.js"; +import type { AgentMessage } from "../runtime/index.js"; import { log } from "./logger.js"; function resolvePostCompactionIndexSyncMode(config?: OpenClawConfig): "off" | "async" | "await" { diff --git a/src/agents/pi-embedded-runner/compaction-runtime-context.test.ts b/src/agents/embedded-agent-runner/compaction-runtime-context.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/compaction-runtime-context.test.ts rename to src/agents/embedded-agent-runner/compaction-runtime-context.test.ts diff --git a/src/agents/pi-embedded-runner/compaction-runtime-context.ts b/src/agents/embedded-agent-runner/compaction-runtime-context.ts similarity index 100% rename from src/agents/pi-embedded-runner/compaction-runtime-context.ts rename to src/agents/embedded-agent-runner/compaction-runtime-context.ts diff --git a/src/agents/pi-embedded-runner/compaction-safety-timeout.ts b/src/agents/embedded-agent-runner/compaction-safety-timeout.ts similarity index 100% rename from src/agents/pi-embedded-runner/compaction-safety-timeout.ts rename to src/agents/embedded-agent-runner/compaction-safety-timeout.ts diff --git a/src/agents/pi-embedded-runner/compaction-successor-transcript.test.ts b/src/agents/embedded-agent-runner/compaction-successor-transcript.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/compaction-successor-transcript.test.ts rename to src/agents/embedded-agent-runner/compaction-successor-transcript.test.ts index 987fb8ded95..18de82e30f2 100644 --- a/src/agents/pi-embedded-runner/compaction-successor-transcript.test.ts +++ b/src/agents/embedded-agent-runner/compaction-successor-transcript.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, describe, expect, it, vi } from "vitest"; import { makeAgentAssistantMessage } from "../test-helpers/agent-message-fixtures.js"; import { diff --git a/src/agents/pi-embedded-runner/compaction-successor-transcript.ts b/src/agents/embedded-agent-runner/compaction-successor-transcript.ts similarity index 99% rename from src/agents/pi-embedded-runner/compaction-successor-transcript.ts rename to src/agents/embedded-agent-runner/compaction-successor-transcript.ts index 651453c00aa..7753adb50ce 100644 --- a/src/agents/pi-embedded-runner/compaction-successor-transcript.ts +++ b/src/agents/embedded-agent-runner/compaction-successor-transcript.ts @@ -1,12 +1,12 @@ import { randomUUID } from "node:crypto"; import path from "node:path"; +import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { CURRENT_SESSION_VERSION, type CompactionEntry, type SessionEntry, type SessionHeader, -} from "@earendil-works/pi-coding-agent"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; +} from "../sessions/index.js"; import { collectDuplicateUserMessageEntryIdsForCompaction } from "./compaction-duplicate-user-messages.js"; import { readTranscriptFileState, diff --git a/src/agents/pi-embedded-runner/context-engine-capabilities.ts b/src/agents/embedded-agent-runner/context-engine-capabilities.ts similarity index 100% rename from src/agents/pi-embedded-runner/context-engine-capabilities.ts rename to src/agents/embedded-agent-runner/context-engine-capabilities.ts diff --git a/src/agents/pi-embedded-runner/context-engine-maintenance.test.ts b/src/agents/embedded-agent-runner/context-engine-maintenance.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/context-engine-maintenance.test.ts rename to src/agents/embedded-agent-runner/context-engine-maintenance.test.ts diff --git a/src/agents/pi-embedded-runner/context-engine-maintenance.ts b/src/agents/embedded-agent-runner/context-engine-maintenance.ts similarity index 100% rename from src/agents/pi-embedded-runner/context-engine-maintenance.ts rename to src/agents/embedded-agent-runner/context-engine-maintenance.ts diff --git a/src/agents/pi-embedded-runner/context-truncation-notice.ts b/src/agents/embedded-agent-runner/context-truncation-notice.ts similarity index 100% rename from src/agents/pi-embedded-runner/context-truncation-notice.ts rename to src/agents/embedded-agent-runner/context-truncation-notice.ts diff --git a/src/agents/pi-embedded-runner/delivery-evidence.ts b/src/agents/embedded-agent-runner/delivery-evidence.ts similarity index 100% rename from src/agents/pi-embedded-runner/delivery-evidence.ts rename to src/agents/embedded-agent-runner/delivery-evidence.ts diff --git a/src/agents/pi-embedded-runner/effective-tool-policy.test.ts b/src/agents/embedded-agent-runner/effective-tool-policy.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/effective-tool-policy.test.ts rename to src/agents/embedded-agent-runner/effective-tool-policy.test.ts diff --git a/src/agents/pi-embedded-runner/effective-tool-policy.ts b/src/agents/embedded-agent-runner/effective-tool-policy.ts similarity index 99% rename from src/agents/pi-embedded-runner/effective-tool-policy.ts rename to src/agents/embedded-agent-runner/effective-tool-policy.ts index c2ffe23f71c..62d835b13fd 100644 --- a/src/agents/pi-embedded-runner/effective-tool-policy.ts +++ b/src/agents/embedded-agent-runner/effective-tool-policy.ts @@ -6,7 +6,7 @@ import { resolveInheritedToolPolicyForSession, resolveTrustedGroupId, resolveSubagentToolPolicyForSession, -} from "../pi-tools.policy.js"; +} from "../agent-tools.policy.js"; import { resolveSenderToolPolicy } from "../sender-tool-policy.js"; import { isSubagentEnvelopeSession, diff --git a/src/agents/pi-embedded-runner/empty-assistant-turn.ts b/src/agents/embedded-agent-runner/empty-assistant-turn.ts similarity index 95% rename from src/agents/pi-embedded-runner/empty-assistant-turn.ts rename to src/agents/embedded-agent-runner/empty-assistant-turn.ts index 2a77a962a59..5b1d7d65db5 100644 --- a/src/agents/pi-embedded-runner/empty-assistant-turn.ts +++ b/src/agents/embedded-agent-runner/empty-assistant-turn.ts @@ -16,7 +16,7 @@ type UsageFieldMap = { total_tokens?: unknown; }; -// Upstream badlogic/pi-mono should normalize Anthropic zero-token empty `stop` +// Upstream agent runtimes should normalize Anthropic zero-token empty `stop` // turns before OpenClaw sees them. Downstream: openclaw/openclaw#71880. function readFiniteTokenCount(value: unknown): number | undefined { return asFiniteNumber(value); diff --git a/src/agents/pi-embedded-runner/execution-phase.ts b/src/agents/embedded-agent-runner/execution-phase.ts similarity index 100% rename from src/agents/pi-embedded-runner/execution-phase.ts rename to src/agents/embedded-agent-runner/execution-phase.ts diff --git a/src/agents/pi-embedded-runner/extensions.test.ts b/src/agents/embedded-agent-runner/extensions.test.ts similarity index 90% rename from src/agents/pi-embedded-runner/extensions.test.ts rename to src/agents/embedded-agent-runner/extensions.test.ts index 2fccb2d7d08..094ed92f1fe 100644 --- a/src/agents/pi-embedded-runner/extensions.test.ts +++ b/src/agents/embedded-agent-runner/extensions.test.ts @@ -1,10 +1,10 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; -import type { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; -import { getCompactionSafeguardRuntime } from "../pi-hooks/compaction-safeguard-runtime.js"; -import compactionSafeguardExtension from "../pi-hooks/compaction-safeguard.js"; -import contextPruningExtension from "../pi-hooks/context-pruning.js"; +import { getCompactionSafeguardRuntime } from "../agent-hooks/compaction-safeguard-runtime.js"; +import compactionSafeguardExtension from "../agent-hooks/compaction-safeguard.js"; +import contextPruningExtension from "../agent-hooks/context-pruning.js"; import { buildEmbeddedExtensionFactories } from "./extensions.js"; vi.mock("../../plugins/provider-runtime.js", () => ({ @@ -21,7 +21,7 @@ function buildSafeguardFactories(cfg: OpenClawConfig, workspaceDir?: string) { const model = { id: "claude-sonnet-4-20250514", contextWindow: 200_000, - } as Model; + } as Model; const factories = buildEmbeddedExtensionFactories({ cfg, @@ -135,7 +135,7 @@ describe("buildEmbeddedExtensionFactories", () => { sessionManager: {} as SessionManager, provider: "litellm", modelId: "claude-sonnet-4-6", - model: { api: "anthropic-messages", contextWindow: 200_000 } as Model, + model: { api: "anthropic-messages", contextWindow: 200_000 } as Model, }); expect(factories).toContain(contextPruningExtension); diff --git a/src/agents/pi-embedded-runner/extensions.ts b/src/agents/embedded-agent-runner/extensions.ts similarity index 85% rename from src/agents/pi-embedded-runner/extensions.ts rename to src/agents/embedded-agent-runner/extensions.ts index d403ebf8dc0..38743383888 100644 --- a/src/agents/pi-embedded-runner/extensions.ts +++ b/src/agents/embedded-agent-runner/extensions.ts @@ -1,23 +1,26 @@ import { randomUUID } from "node:crypto"; -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; -import type { ExtensionFactory, SessionManager } from "@earendil-works/pi-coding-agent"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js"; import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; +import { setCompactionSafeguardRuntime } from "../agent-hooks/compaction-safeguard-runtime.js"; +import compactionSafeguardExtension from "../agent-hooks/compaction-safeguard.js"; +import contextPruningExtension from "../agent-hooks/context-pruning.js"; +import { setContextPruningRuntime } from "../agent-hooks/context-pruning/runtime.js"; +import { computeEffectiveSettings } from "../agent-hooks/context-pruning/settings.js"; +import { makeToolPrunablePredicate } from "../agent-hooks/context-pruning/tools.js"; +import { + ensureAgentCompactionReserveTokens, + resolveEffectiveCompactionMode, +} from "../agent-settings.js"; import { resolveContextWindowInfo } from "../context-window-guard.js"; import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js"; import { createAgentToolResultMiddlewareRunner } from "../harness/tool-result-middleware.js"; -import { setCompactionSafeguardRuntime } from "../pi-hooks/compaction-safeguard-runtime.js"; -import compactionSafeguardExtension from "../pi-hooks/compaction-safeguard.js"; -import contextPruningExtension from "../pi-hooks/context-pruning.js"; -import { setContextPruningRuntime } from "../pi-hooks/context-pruning/runtime.js"; -import { computeEffectiveSettings } from "../pi-hooks/context-pruning/settings.js"; -import { makeToolPrunablePredicate } from "../pi-hooks/context-pruning/tools.js"; -import { ensurePiCompactionReserveTokens, resolveEffectiveCompactionMode } from "../pi-settings.js"; +import type { AgentToolResult } from "../runtime/index.js"; +import type { ExtensionFactory, SessionManager } from "../sessions/index.js"; import { resolveTranscriptPolicy } from "../transcript-policy.js"; import { isCacheTtlEligibleProvider, readLastCacheTtlTimestamp } from "./cache-ttl.js"; -type PiToolResultEvent = { +type AgentToolResultEvent = { threadId?: string; turnId?: string; toolCallId?: string; @@ -45,17 +48,17 @@ function hasErrorToolResultStatus(result: AgentToolResult): boolean { } function buildAgentToolResultMiddlewareFactory(): ExtensionFactory { - const runner = createAgentToolResultMiddlewareRunner({ runtime: "pi" }); - return (pi) => { - pi.on("tool_result", async (rawEvent: unknown, ctx: { cwd?: string }) => { - const event = recordFromUnknown(rawEvent) as PiToolResultEvent; + const runner = createAgentToolResultMiddlewareRunner({ runtime: "openclaw" }); + return (agent) => { + agent.on("tool_result", async (rawEvent: unknown, ctx: { cwd?: string }) => { + const event = recordFromUnknown(rawEvent) as AgentToolResultEvent; if (!event.toolName) { return undefined; } const toolCallId = typeof event.toolCallId === "string" && event.toolCallId.trim() ? event.toolCallId - : `pi-${randomUUID()}`; + : `openclaw-${randomUUID()}`; const content = Array.isArray(event.content) ? event.content : []; const current = { content, @@ -182,4 +185,4 @@ export function buildEmbeddedExtensionFactories(params: { return factories; } -export { ensurePiCompactionReserveTokens }; +export { ensureAgentCompactionReserveTokens }; diff --git a/src/agents/pi-embedded-runner/extra-params.cache-retention-default.test.ts b/src/agents/embedded-agent-runner/extra-params.cache-retention-default.test.ts similarity index 96% rename from src/agents/pi-embedded-runner/extra-params.cache-retention-default.test.ts rename to src/agents/embedded-agent-runner/extra-params.cache-retention-default.test.ts index ccdf1b1e5f8..8e2f04fe0bd 100644 --- a/src/agents/pi-embedded-runner/extra-params.cache-retention-default.test.ts +++ b/src/agents/embedded-agent-runner/extra-params.cache-retention-default.test.ts @@ -1,7 +1,7 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createPiAiStreamSimpleMock } from "../../../test/helpers/agents/pi-ai-stream-simple-mock.js"; -import { isOpenRouterAnthropicModelRef } from "./anthropic-family-cache-semantics.js"; +import { createLlmStreamSimpleMock } from "../../../test/helpers/agents/llm-stream-simple-mock.js"; +import { isOpenRouterAnthropicModelRef } from "../../llm/providers/stream-wrappers/anthropic-family-cache-semantics.js"; import { testing as extraParamsTesting, applyExtraParamsToAgent } from "./extra-params.js"; import { resolveCacheRetention } from "./prompt-cache-retention.js"; @@ -39,7 +39,7 @@ vi.mock("./logger.js", () => ({ }, })); -vi.mock("@earendil-works/pi-ai", () => createPiAiStreamSimpleMock()); +vi.mock("openclaw/plugin-sdk/llm", () => createLlmStreamSimpleMock()); beforeEach(() => { extraParamsTesting.setProviderRuntimeDepsForTest({ diff --git a/src/agents/pi-embedded-runner/extra-params.google.test.ts b/src/agents/embedded-agent-runner/extra-params.google.test.ts similarity index 93% rename from src/agents/pi-embedded-runner/extra-params.google.test.ts rename to src/agents/embedded-agent-runner/extra-params.google.test.ts index db66f2b50ce..9a01c82eb8b 100644 --- a/src/agents/pi-embedded-runner/extra-params.google.test.ts +++ b/src/agents/embedded-agent-runner/extra-params.google.test.ts @@ -1,10 +1,10 @@ -import type { Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createPiAiStreamSimpleMock } from "../../../test/helpers/agents/pi-ai-stream-simple-mock.js"; +import { createLlmStreamSimpleMock } from "../../../test/helpers/agents/llm-stream-simple-mock.js"; import { testing as extraParamsTesting } from "./extra-params.js"; import { runExtraParamsCase } from "./extra-params.test-support.js"; -vi.mock("@earendil-works/pi-ai", () => createPiAiStreamSimpleMock()); +vi.mock("openclaw/plugin-sdk/llm", () => createLlmStreamSimpleMock()); beforeEach(() => { extraParamsTesting.setProviderRuntimeDepsForTest({ diff --git a/src/agents/pi-embedded-runner/extra-params.kilocode.test.ts b/src/agents/embedded-agent-runner/extra-params.kilocode.test.ts similarity index 95% rename from src/agents/pi-embedded-runner/extra-params.kilocode.test.ts rename to src/agents/embedded-agent-runner/extra-params.kilocode.test.ts index 221eb59bf03..e7ba5a7560a 100644 --- a/src/agents/pi-embedded-runner/extra-params.kilocode.test.ts +++ b/src/agents/embedded-agent-runner/extra-params.kilocode.test.ts @@ -1,8 +1,11 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; +import type { Context, Model, SimpleStreamOptions } from "openclaw/plugin-sdk/llm"; import { afterEach, describe, expect, it } from "vitest"; +import { + createKilocodeWrapper, + isProxyReasoningUnsupported, +} from "../../llm/providers/stream-wrappers/proxy.js"; import { captureEnv } from "../../test-utils/env.js"; -import { createKilocodeWrapper, isProxyReasoningUnsupported } from "./proxy-stream-wrappers.js"; type ExtraParamsCapture> = { headers?: Record; diff --git a/src/agents/pi-embedded-runner/extra-params.openrouter-cache-control.test.ts b/src/agents/embedded-agent-runner/extra-params.openrouter-cache-control.test.ts similarity index 95% rename from src/agents/pi-embedded-runner/extra-params.openrouter-cache-control.test.ts rename to src/agents/embedded-agent-runner/extra-params.openrouter-cache-control.test.ts index 913155be468..23d84da2d14 100644 --- a/src/agents/pi-embedded-runner/extra-params.openrouter-cache-control.test.ts +++ b/src/agents/embedded-agent-runner/extra-params.openrouter-cache-control.test.ts @@ -1,6 +1,6 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; -import { createOpenRouterSystemCacheWrapper } from "./proxy-stream-wrappers.js"; +import { createOpenRouterSystemCacheWrapper } from "../../llm/providers/stream-wrappers/proxy.js"; type StreamPayload = { messages: Array<{ diff --git a/src/agents/pi-embedded-runner/extra-params.provider-runtime.test.ts b/src/agents/embedded-agent-runner/extra-params.provider-runtime.test.ts similarity index 94% rename from src/agents/pi-embedded-runner/extra-params.provider-runtime.test.ts rename to src/agents/embedded-agent-runner/extra-params.provider-runtime.test.ts index a21e0ee34cf..2e981534df4 100644 --- a/src/agents/pi-embedded-runner/extra-params.provider-runtime.test.ts +++ b/src/agents/embedded-agent-runner/extra-params.provider-runtime.test.ts @@ -1,6 +1,6 @@ -import type { Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createPiAiStreamSimpleMock } from "../../../test/helpers/agents/pi-ai-stream-simple-mock.js"; +import { createLlmStreamSimpleMock } from "../../../test/helpers/agents/llm-stream-simple-mock.js"; import { testing as extraParamsTesting, resolveAgentTransportOverride, @@ -8,7 +8,7 @@ import { } from "./extra-params.js"; import { runExtraParamsCase } from "./extra-params.test-support.js"; -vi.mock("@earendil-works/pi-ai", () => createPiAiStreamSimpleMock()); +vi.mock("openclaw/plugin-sdk/llm", () => createLlmStreamSimpleMock()); beforeEach(() => { extraParamsTesting.setProviderRuntimeDepsForTest({ diff --git a/src/agents/pi-embedded-runner/extra-params.sampling.test.ts b/src/agents/embedded-agent-runner/extra-params.sampling.test.ts similarity index 98% rename from src/agents/pi-embedded-runner/extra-params.sampling.test.ts rename to src/agents/embedded-agent-runner/extra-params.sampling.test.ts index bddd6866259..472a18f58e7 100644 --- a/src/agents/pi-embedded-runner/extra-params.sampling.test.ts +++ b/src/agents/embedded-agent-runner/extra-params.sampling.test.ts @@ -1,6 +1,6 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createPiAiStreamSimpleMock } from "../../../test/helpers/agents/pi-ai-stream-simple-mock.js"; +import { createLlmStreamSimpleMock } from "../../../test/helpers/agents/llm-stream-simple-mock.js"; import { testing as extraParamsTesting, applyExtraParamsToAgent, @@ -15,7 +15,7 @@ vi.mock("./logger.js", () => ({ }, })); -vi.mock("@earendil-works/pi-ai", () => createPiAiStreamSimpleMock()); +vi.mock("openclaw/plugin-sdk/llm", () => createLlmStreamSimpleMock()); beforeEach(() => { extraParamsTesting.setProviderRuntimeDepsForTest({ diff --git a/src/agents/pi-embedded-runner/extra-params.test-support.ts b/src/agents/embedded-agent-runner/extra-params.test-support.ts similarity index 94% rename from src/agents/pi-embedded-runner/extra-params.test-support.ts rename to src/agents/embedded-agent-runner/extra-params.test-support.ts index 0dd6810be4e..5dacc2023ea 100644 --- a/src/agents/pi-embedded-runner/extra-params.test-support.ts +++ b/src/agents/embedded-agent-runner/extra-params.test-support.ts @@ -1,7 +1,7 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai"; +import type { Context, Model, SimpleStreamOptions } from "openclaw/plugin-sdk/llm"; import type { ThinkLevel } from "../../auto-reply/thinking.shared.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import type { StreamFn } from "../runtime/index.js"; import { testing as extraParamsTesting, applyExtraParamsToAgent } from "./extra-params.js"; export type ExtraParamsCapture> = { diff --git a/src/agents/pi-embedded-runner/extra-params.ts b/src/agents/embedded-agent-runner/extra-params.ts similarity index 97% rename from src/agents/pi-embedded-runner/extra-params.ts rename to src/agents/embedded-agent-runner/extra-params.ts index d767936af9d..39dce3f0fa3 100644 --- a/src/agents/pi-embedded-runner/extra-params.ts +++ b/src/agents/embedded-agent-runner/extra-params.ts @@ -1,9 +1,21 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { SimpleStreamOptions } from "@earendil-works/pi-ai"; -import { streamSimple } from "@earendil-works/pi-ai"; -import type { SettingsManager } from "@earendil-works/pi-coding-agent"; +import type { SimpleStreamOptions } from "openclaw/plugin-sdk/llm"; +import { streamSimple } from "openclaw/plugin-sdk/llm"; import type { ThinkLevel } from "../../auto-reply/thinking.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { createGoogleThinkingPayloadWrapper } from "../../llm/providers/stream-wrappers/google.js"; +import { createMinimaxThinkingDisabledWrapper } from "../../llm/providers/stream-wrappers/minimax.js"; +import { + createSiliconFlowThinkingWrapper, + shouldApplySiliconFlowThinkingOffCompat, +} from "../../llm/providers/stream-wrappers/moonshot.js"; +import { + createOpenAICompletionsStrictMessageKeysWrapper, + createOpenAICompletionsToolsCompatWrapper, + createOpenAIResponsesContextManagementWrapper, + createOpenAIStringContentWrapper, +} from "../../llm/providers/stream-wrappers/openai.js"; +import { createOpenRouterSystemCacheWrapper } from "../../llm/providers/stream-wrappers/proxy.js"; +import { streamWithPayloadPatch } from "../../llm/providers/stream-wrappers/stream-payload-utils.js"; import { createDeepSeekV4OpenAICompatibleThinkingWrapper, createThinkingOnlyFinalTextWrapper, @@ -20,22 +32,10 @@ import { legacyModelKey, modelKey } from "../model-selection-normalize.js"; import { supportsGptParallelToolCallsPayload } from "../provider-api-families.js"; import { resolveProviderRequestPolicyConfig } from "../provider-request-config.js"; import type { AgentRuntimeTransport } from "../runtime-plan/types.js"; -import { createGoogleThinkingPayloadWrapper } from "./google-stream-wrappers.js"; +import type { StreamFn } from "../runtime/index.js"; +import type { SettingsManager } from "../sessions/index.js"; import { log } from "./logger.js"; -import { createMinimaxThinkingDisabledWrapper } from "./minimax-stream-wrappers.js"; -import { - createSiliconFlowThinkingWrapper, - shouldApplySiliconFlowThinkingOffCompat, -} from "./moonshot-stream-wrappers.js"; -import { - createOpenAICompletionsStrictMessageKeysWrapper, - createOpenAICompletionsToolsCompatWrapper, - createOpenAIResponsesContextManagementWrapper, - createOpenAIStringContentWrapper, -} from "./openai-stream-wrappers.js"; import { resolveCacheRetention } from "./prompt-cache-retention.js"; -import { createOpenRouterSystemCacheWrapper } from "./proxy-stream-wrappers.js"; -import { streamWithPayloadPatch } from "./stream-payload-utils.js"; const defaultProviderRuntimeDeps = { prepareProviderExtraParams: prepareProviderExtraParamsRuntime, @@ -819,7 +819,7 @@ function applyPostPluginStreamWrappers( // emitted by upstream model-ID heuristics for Gemini 3.1 variants. ctx.agent.streamFn = createGoogleThinkingPayloadWrapper(ctx.agent.streamFn, ctx.thinkingLevel); - // Work around upstream pi-ai hardcoding `store: false` for Responses API. + // Work around upstream shared model runtime hardcoding `store: false` for Responses API. // Force `store=true` for direct OpenAI Responses models and auto-enable // server-side compaction for compatible Responses payloads. ctx.agent.streamFn = createOpenAIResponsesContextManagementWrapper( diff --git a/src/agents/pi-embedded-runner/extra-params.zai-tool-stream.test.ts b/src/agents/embedded-agent-runner/extra-params.zai-tool-stream.test.ts similarity index 95% rename from src/agents/pi-embedded-runner/extra-params.zai-tool-stream.test.ts rename to src/agents/embedded-agent-runner/extra-params.zai-tool-stream.test.ts index c4c4343ccf0..8f314827391 100644 --- a/src/agents/pi-embedded-runner/extra-params.zai-tool-stream.test.ts +++ b/src/agents/embedded-agent-runner/extra-params.zai-tool-stream.test.ts @@ -1,9 +1,9 @@ -import type { Model, SimpleStreamOptions } from "@earendil-works/pi-ai"; +import type { Model, SimpleStreamOptions } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { createPiAiStreamSimpleMock } from "../../../test/helpers/agents/pi-ai-stream-simple-mock.js"; +import { createLlmStreamSimpleMock } from "../../../test/helpers/agents/llm-stream-simple-mock.js"; import type { OpenClawConfig } from "../../config/config.js"; -vi.mock("@earendil-works/pi-ai", () => createPiAiStreamSimpleMock()); +vi.mock("openclaw/plugin-sdk/llm", () => createLlmStreamSimpleMock()); let runExtraParamsCase: typeof import("./extra-params.test-support.js").runExtraParamsCase; let extraParamsTesting: typeof import("./extra-params.js").testing; diff --git a/src/agents/pi-embedded-runner/failure-signal.test.ts b/src/agents/embedded-agent-runner/failure-signal.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/failure-signal.test.ts rename to src/agents/embedded-agent-runner/failure-signal.test.ts diff --git a/src/agents/pi-embedded-runner/failure-signal.ts b/src/agents/embedded-agent-runner/failure-signal.ts similarity index 100% rename from src/agents/pi-embedded-runner/failure-signal.ts rename to src/agents/embedded-agent-runner/failure-signal.ts diff --git a/src/agents/pi-embedded-runner/google-prompt-cache.test.ts b/src/agents/embedded-agent-runner/google-prompt-cache.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/google-prompt-cache.test.ts rename to src/agents/embedded-agent-runner/google-prompt-cache.test.ts index 307d36e3727..5e99f2a9057 100644 --- a/src/agents/pi-embedded-runner/google-prompt-cache.test.ts +++ b/src/agents/embedded-agent-runner/google-prompt-cache.test.ts @@ -1,6 +1,6 @@ import crypto from "node:crypto"; -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { Model } from "@earendil-works/pi-ai"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import { prepareGooglePromptCacheStreamFn } from "./google-prompt-cache.js"; import { EmbeddedAttemptSessionTakeoverError } from "./run/attempt.session-lock.js"; diff --git a/src/agents/pi-embedded-runner/google-prompt-cache.ts b/src/agents/embedded-agent-runner/google-prompt-cache.ts similarity index 98% rename from src/agents/pi-embedded-runner/google-prompt-cache.ts rename to src/agents/embedded-agent-runner/google-prompt-cache.ts index a945beab7ca..30af3b25fae 100644 --- a/src/agents/pi-embedded-runner/google-prompt-cache.ts +++ b/src/agents/embedded-agent-runner/google-prompt-cache.ts @@ -1,10 +1,11 @@ import crypto from "node:crypto"; -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { parseGeminiAuth } from "../../infra/gemini-auth.js"; import { normalizeGoogleApiBaseUrl } from "../../infra/google-api-base-url.js"; +import { streamWithPayloadPatch } from "../../llm/providers/stream-wrappers/stream-payload-utils.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { buildGuardedModelFetch } from "../provider-transport-fetch.js"; +import type { StreamFn } from "../runtime/index.js"; import { isSessionWriteLockTimeoutError } from "../session-write-lock-error.js"; import { stableStringify } from "../stable-stringify.js"; import { stripSystemPromptCacheBoundary } from "../system-prompt-cache-boundary.js"; @@ -12,7 +13,6 @@ import { mergeTransportHeaders, sanitizeTransportPayloadText } from "../transpor import { log } from "./logger.js"; import { isGooglePromptCacheEligible, resolveCacheRetention } from "./prompt-cache-retention.js"; import { EmbeddedAttemptSessionTakeoverError } from "./run/attempt.session-lock.js"; -import { streamWithPayloadPatch } from "./stream-payload-utils.js"; const GOOGLE_PROMPT_CACHE_CUSTOM_TYPE = "openclaw.google-prompt-cache"; const GOOGLE_PROMPT_CACHE_RETRY_BACKOFF_MS = 10 * 60_000; @@ -26,7 +26,7 @@ type GooglePromptCacheSessionManager = { appendCustomEntry(customType: string, data?: unknown): void | Promise; getEntries(): CustomEntryLike[]; }; -type GooglePromptCacheModel = Model & { +type GooglePromptCacheModel = Model & { baseUrl?: string; headers?: Record; provider: string; diff --git a/src/agents/pi-embedded-runner/history.test.ts b/src/agents/embedded-agent-runner/history.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/history.test.ts rename to src/agents/embedded-agent-runner/history.test.ts diff --git a/src/agents/pi-embedded-runner/history.ts b/src/agents/embedded-agent-runner/history.ts similarity index 98% rename from src/agents/pi-embedded-runner/history.ts rename to src/agents/embedded-agent-runner/history.ts index bad1f48d5ab..5485e391c11 100644 --- a/src/agents/pi-embedded-runner/history.ts +++ b/src/agents/embedded-agent-runner/history.ts @@ -1,7 +1,7 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; import { normalizeProviderId } from "../provider-id.js"; +import type { AgentMessage } from "../runtime/index.js"; const THREAD_SUFFIX_REGEX = /^(.*)(?::(?:thread|topic):\d+)$/i; diff --git a/src/agents/pi-embedded-runner/kilocode.test.ts b/src/agents/embedded-agent-runner/kilocode.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/kilocode.test.ts rename to src/agents/embedded-agent-runner/kilocode.test.ts diff --git a/src/agents/pi-embedded-runner/lanes.test.ts b/src/agents/embedded-agent-runner/lanes.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/lanes.test.ts rename to src/agents/embedded-agent-runner/lanes.test.ts diff --git a/src/agents/pi-embedded-runner/lanes.ts b/src/agents/embedded-agent-runner/lanes.ts similarity index 100% rename from src/agents/pi-embedded-runner/lanes.ts rename to src/agents/embedded-agent-runner/lanes.ts diff --git a/src/agents/pi-embedded-runner/logger.ts b/src/agents/embedded-agent-runner/logger.ts similarity index 100% rename from src/agents/pi-embedded-runner/logger.ts rename to src/agents/embedded-agent-runner/logger.ts diff --git a/src/agents/pi-embedded-runner/manual-compaction-boundary.test.ts b/src/agents/embedded-agent-runner/manual-compaction-boundary.test.ts similarity index 97% rename from src/agents/pi-embedded-runner/manual-compaction-boundary.test.ts rename to src/agents/embedded-agent-runner/manual-compaction-boundary.test.ts index 4a95c9c8ce6..ec34aa29d5a 100644 --- a/src/agents/pi-embedded-runner/manual-compaction-boundary.test.ts +++ b/src/agents/embedded-agent-runner/manual-compaction-boundary.test.ts @@ -1,9 +1,9 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage } from "@earendil-works/pi-ai"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { afterEach, describe, expect, it, vi } from "vitest"; import { hardenManualCompactionBoundary } from "./manual-compaction-boundary.js"; diff --git a/src/agents/pi-embedded-runner/manual-compaction-boundary.ts b/src/agents/embedded-agent-runner/manual-compaction-boundary.ts similarity index 96% rename from src/agents/pi-embedded-runner/manual-compaction-boundary.ts rename to src/agents/embedded-agent-runner/manual-compaction-boundary.ts index dd5fe449941..8e01da378b0 100644 --- a/src/agents/pi-embedded-runner/manual-compaction-boundary.ts +++ b/src/agents/embedded-agent-runner/manual-compaction-boundary.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { SessionEntry } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "../runtime/index.js"; +import type { SessionEntry } from "../sessions/index.js"; import { readTranscriptFileState, TranscriptFileState, diff --git a/src/agents/pi-embedded-runner/message-action-discovery-input.test.ts b/src/agents/embedded-agent-runner/message-action-discovery-input.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/message-action-discovery-input.test.ts rename to src/agents/embedded-agent-runner/message-action-discovery-input.test.ts diff --git a/src/agents/pi-embedded-runner/message-action-discovery-input.ts b/src/agents/embedded-agent-runner/message-action-discovery-input.ts similarity index 100% rename from src/agents/pi-embedded-runner/message-action-discovery-input.ts rename to src/agents/embedded-agent-runner/message-action-discovery-input.ts diff --git a/src/agents/embedded-agent-runner/model-context-tokens.ts b/src/agents/embedded-agent-runner/model-context-tokens.ts new file mode 100644 index 00000000000..86ce11e91da --- /dev/null +++ b/src/agents/embedded-agent-runner/model-context-tokens.ts @@ -0,0 +1,10 @@ +import type { Model } from "openclaw/plugin-sdk/llm"; + +type AgentModelWithOptionalContextTokens = Model & { + contextTokens?: number; +}; + +export function readAgentModelContextTokens(model: Model | null | undefined): number | undefined { + const value = (model as AgentModelWithOptionalContextTokens | null | undefined)?.contextTokens; + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} diff --git a/src/agents/pi-embedded-runner/model-discovery-cache.ts b/src/agents/embedded-agent-runner/model-discovery-cache.ts similarity index 86% rename from src/agents/pi-embedded-runner/model-discovery-cache.ts rename to src/agents/embedded-agent-runner/model-discovery-cache.ts index bfb3ef5e432..081d1db41b7 100644 --- a/src/agents/pi-embedded-runner/model-discovery-cache.ts +++ b/src/agents/embedded-agent-runner/model-discovery-cache.ts @@ -1,20 +1,20 @@ import { statSync } from "node:fs"; import path from "node:path"; -import type { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent"; import { resolveRuntimeExternalAuthProviderRefs, resolveRuntimeSyntheticAuthProviderRefs, } from "../../plugins/synthetic-auth.runtime.js"; +import { discoverAuthStorage, discoverModels } from "../agent-model-discovery.js"; import { resolveDefaultAgentDir } from "../agent-scope.js"; import { hasAnyRuntimeAuthProfileStoreSource } from "../auth-profiles/runtime-snapshots.js"; -import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js"; +import type { AuthStorage, ModelRegistry } from "../sessions/index.js"; type DiscoveryStores = { authStorage: AuthStorage; modelRegistry: ModelRegistry; }; -type DiscoverCachedPiStoresOptions = { +type DiscoverCachedAgentStoresOptions = { agentDir: string; inheritedAuthDir?: string; }; @@ -47,7 +47,7 @@ function authFingerprint(agentDir: string): object { }; } -function discoveryFingerprint(params: DiscoverCachedPiStoresOptions): string { +function discoveryFingerprint(params: DiscoverCachedAgentStoresOptions): string { const inheritedAuthDir = params.inheritedAuthDir && params.inheritedAuthDir !== params.agentDir ? params.inheritedAuthDir @@ -82,19 +82,21 @@ function pruneDiscoveryStoreCache(): void { } } -function discoverFreshPiStores(agentDir: string): DiscoveryStores { +function discoverFreshAgentStores(agentDir: string): DiscoveryStores { const authStorage = discoverAuthStorage(agentDir); const modelRegistry = discoverModels(authStorage, agentDir); return { authStorage, modelRegistry }; } -export function discoverCachedPiStores(options: DiscoverCachedPiStoresOptions): DiscoveryStores { +export function discoverCachedAgentStores( + options: DiscoverCachedAgentStoresOptions, +): DiscoveryStores { const agentDir = normalizeCacheDir(options.agentDir) ?? options.agentDir; const inheritedAuthDir = normalizeCacheDir( options.inheritedAuthDir ?? resolveDefaultAgentDir({}), ); if (hasAnyRuntimeAuthProfileStoreSource(agentDir) || hasRuntimePluginAuthSources()) { - return discoverFreshPiStores(agentDir); + return discoverFreshAgentStores(agentDir); } const cacheKey = JSON.stringify({ agentDir, inheritedAuthDir }); @@ -108,7 +110,7 @@ export function discoverCachedPiStores(options: DiscoverCachedPiStoresOptions): }; } - const stores = discoverFreshPiStores(agentDir); + const stores = discoverFreshAgentStores(agentDir); DISCOVERY_STORE_CACHE.set(cacheKey, { authStorage: stores.authStorage, fingerprint, diff --git a/src/agents/pi-embedded-runner/model.forward-compat.errors-and-overrides.test.ts b/src/agents/embedded-agent-runner/model.forward-compat.errors-and-overrides.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/model.forward-compat.errors-and-overrides.test.ts rename to src/agents/embedded-agent-runner/model.forward-compat.errors-and-overrides.test.ts index 32b8aeb397d..93100933dc4 100644 --- a/src/agents/pi-embedded-runner/model.forward-compat.errors-and-overrides.test.ts +++ b/src/agents/embedded-agent-runner/model.forward-compat.errors-and-overrides.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ModelProviderConfig } from "../../config/config.js"; -import { discoverModels } from "../pi-model-discovery.js"; +import { discoverModels } from "../agent-model-discovery.js"; import { createProviderRuntimeTestMock } from "./model.provider-runtime.test-support.js"; vi.mock("../../plugins/provider-runtime.js", async () => { @@ -38,7 +38,7 @@ vi.mock("../model-suppression.js", () => ({ }, })); -vi.mock("../pi-model-discovery.js", () => ({ +vi.mock("../agent-model-discovery.js", () => ({ discoverAuthStorage: vi.fn(() => ({ mocked: true })), discoverModels: vi.fn(() => ({ find: vi.fn(() => null) })), })); diff --git a/src/agents/pi-embedded-runner/model.forward-compat.test-support.ts b/src/agents/embedded-agent-runner/model.forward-compat.test-support.ts similarity index 100% rename from src/agents/pi-embedded-runner/model.forward-compat.test-support.ts rename to src/agents/embedded-agent-runner/model.forward-compat.test-support.ts diff --git a/src/agents/pi-embedded-runner/model.forward-compat.test.ts b/src/agents/embedded-agent-runner/model.forward-compat.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/model.forward-compat.test.ts rename to src/agents/embedded-agent-runner/model.forward-compat.test.ts diff --git a/src/agents/pi-embedded-runner/model.inline-provider.test.ts b/src/agents/embedded-agent-runner/model.inline-provider.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/model.inline-provider.test.ts rename to src/agents/embedded-agent-runner/model.inline-provider.test.ts diff --git a/src/agents/pi-embedded-runner/model.inline-provider.ts b/src/agents/embedded-agent-runner/model.inline-provider.ts similarity index 99% rename from src/agents/pi-embedded-runner/model.inline-provider.ts rename to src/agents/embedded-agent-runner/model.inline-provider.ts index 4c361b14545..4659c38d1b9 100644 --- a/src/agents/pi-embedded-runner/model.inline-provider.ts +++ b/src/agents/embedded-agent-runner/model.inline-provider.ts @@ -1,4 +1,4 @@ -import type { Api } from "@earendil-works/pi-ai"; +import type { Api } from "openclaw/plugin-sdk/llm"; import type { ModelDefinitionConfig, ModelProviderConfig } from "../../config/types.js"; import { normalizeGoogleApiBaseUrl } from "../../infra/google-api-base-url.js"; import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; diff --git a/src/agents/embedded-agent-runner/model.provider-normalization.ts b/src/agents/embedded-agent-runner/model.provider-normalization.ts new file mode 100644 index 00000000000..7a7a6070401 --- /dev/null +++ b/src/agents/embedded-agent-runner/model.provider-normalization.ts @@ -0,0 +1,6 @@ +import type { Model } from "openclaw/plugin-sdk/llm"; +import { normalizeModelCompat } from "../../plugins/provider-model-compat.js"; + +export function normalizeResolvedProviderModel(params: { provider: string; model: Model }): Model { + return normalizeModelCompat(params.model); +} diff --git a/src/agents/pi-embedded-runner/model.provider-runtime.test-support.ts b/src/agents/embedded-agent-runner/model.provider-runtime.test-support.ts similarity index 100% rename from src/agents/pi-embedded-runner/model.provider-runtime.test-support.ts rename to src/agents/embedded-agent-runner/model.provider-runtime.test-support.ts diff --git a/src/agents/pi-embedded-runner/model.skip-pi-discovery-hooks.test.ts b/src/agents/embedded-agent-runner/model.skip-agent-discovery-hooks.test.ts similarity index 92% rename from src/agents/pi-embedded-runner/model.skip-pi-discovery-hooks.test.ts rename to src/agents/embedded-agent-runner/model.skip-agent-discovery-hooks.test.ts index 215fcd5db36..e7d6ca6deff 100644 --- a/src/agents/pi-embedded-runner/model.skip-pi-discovery-hooks.test.ts +++ b/src/agents/embedded-agent-runner/model.skip-agent-discovery-hooks.test.ts @@ -4,15 +4,15 @@ const mocks = vi.hoisted(() => ({ discoverAuthStorage: vi.fn(() => ({ mocked: true })), discoverModels: vi.fn(() => ({ find: vi.fn(() => null) })), applyProviderResolvedModelCompatWithPlugins: vi.fn(() => { - throw new Error("compat hook should not run during skipPiDiscovery"); + throw new Error("compat hook should not run during skipAgentDiscovery"); }), applyProviderResolvedTransportWithPlugin: vi.fn(() => { - throw new Error("transport hook should not run during skipPiDiscovery"); + throw new Error("transport hook should not run during skipAgentDiscovery"); }), buildProviderUnknownModelHintWithPlugin: vi.fn(() => undefined), normalizeProviderResolvedModelWithPlugin: vi.fn(() => undefined), normalizeProviderTransportWithPlugin: vi.fn(() => { - throw new Error("transport normalization hook should not run during skipPiDiscovery"); + throw new Error("transport normalization hook should not run during skipAgentDiscovery"); }), prepareProviderDynamicModel: vi.fn(async () => undefined), runProviderDynamicModel: vi.fn( @@ -32,7 +32,7 @@ const mocks = vi.hoisted(() => ({ shouldPreferProviderRuntimeResolvedModel: vi.fn(() => false), })); -vi.mock("../pi-model-discovery.js", () => ({ +vi.mock("../agent-model-discovery.js", () => ({ discoverAuthStorage: mocks.discoverAuthStorage, discoverModels: mocks.discoverModels, })); @@ -70,10 +70,10 @@ beforeEach(async () => { ({ resolveModelAsync } = await import("./model.js")); }); -describe("resolveModelAsync skipPiDiscovery runtime hooks", () => { +describe("resolveModelAsync skipAgentDiscovery runtime hooks", () => { it("uses only target-provider dynamic hooks", async () => { const result = await resolveModelAsync("ollama", "llama3.2:latest", "/tmp/agent", undefined, { - skipPiDiscovery: true, + skipAgentDiscovery: true, workspaceDir: "/tmp/workspace", }); diff --git a/src/agents/pi-embedded-runner/model.startup-retry.test.ts b/src/agents/embedded-agent-runner/model.startup-retry.test.ts similarity index 98% rename from src/agents/pi-embedded-runner/model.startup-retry.test.ts rename to src/agents/embedded-agent-runner/model.startup-retry.test.ts index 77b3c5de1b7..baa5370a10f 100644 --- a/src/agents/pi-embedded-runner/model.startup-retry.test.ts +++ b/src/agents/embedded-agent-runner/model.startup-retry.test.ts @@ -26,7 +26,7 @@ const runProviderDynamicModelMock = vi.fn<(params: unknown) => unknown>(() => : undefined, ); -vi.mock("../pi-model-discovery.js", () => ({ +vi.mock("../agent-model-discovery.js", () => ({ discoverAuthStorage: discoverAuthStorageMock, discoverModels: discoverModelsMock, })); diff --git a/src/agents/pi-embedded-runner/model.static-catalog.test.ts b/src/agents/embedded-agent-runner/model.static-catalog.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/model.static-catalog.test.ts rename to src/agents/embedded-agent-runner/model.static-catalog.test.ts diff --git a/src/agents/pi-embedded-runner/model.static-catalog.ts b/src/agents/embedded-agent-runner/model.static-catalog.ts similarity index 96% rename from src/agents/pi-embedded-runner/model.static-catalog.ts rename to src/agents/embedded-agent-runner/model.static-catalog.ts index ac7e26b959e..751b703815a 100644 --- a/src/agents/pi-embedded-runner/model.static-catalog.ts +++ b/src/agents/embedded-agent-runner/model.static-catalog.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { planManifestModelCatalogRows } from "../../model-catalog/manifest-planner.js"; import type { NormalizedModelCatalogRow } from "../../model-catalog/types.js"; @@ -22,7 +22,7 @@ function rowMatchesModel(params: { ); } -function modelFromStaticCatalogRow(row: NormalizedModelCatalogRow): Model { +function modelFromStaticCatalogRow(row: NormalizedModelCatalogRow): Model { return { id: row.id, name: row.name || row.id, @@ -38,7 +38,7 @@ function modelFromStaticCatalogRow(row: NormalizedModelCatalogRow): Model { headers: row.headers, compat: row.compat, mediaInput: row.mediaInput, - } as Model; + } as Model; } type StaticCatalogPlugin = Parameters< @@ -94,7 +94,7 @@ export function resolveBundledStaticCatalogModel(params: { cfg?: OpenClawConfig; workspaceDir?: string; env?: NodeJS.ProcessEnv; -}): Model | undefined { +}): Model | undefined { const provider = normalizeProviderId(params.provider); if (!provider || !params.modelId.trim()) { return undefined; diff --git a/src/agents/pi-embedded-runner/model.test-harness.ts b/src/agents/embedded-agent-runner/model.test-harness.ts similarity index 97% rename from src/agents/pi-embedded-runner/model.test-harness.ts rename to src/agents/embedded-agent-runner/model.test-harness.ts index 7beb9577529..d8eb84af1bf 100644 --- a/src/agents/pi-embedded-runner/model.test-harness.ts +++ b/src/agents/embedded-agent-runner/model.test-harness.ts @@ -1,7 +1,7 @@ import { vi } from "vitest"; import type { ModelDefinitionConfig } from "../../config/types.js"; -type DiscoverModelsMock = typeof import("../pi-model-discovery.js").discoverModels; +type DiscoverModelsMock = typeof import("../agent-model-discovery.js").discoverModels; export const makeModel = (id: string): ModelDefinitionConfig => ({ id, diff --git a/src/agents/pi-embedded-runner/model.test.ts b/src/agents/embedded-agent-runner/model.test.ts similarity index 98% rename from src/agents/pi-embedded-runner/model.test.ts rename to src/agents/embedded-agent-runner/model.test.ts index c7cac2f1ad6..5bcf9f0ca62 100644 --- a/src/agents/pi-embedded-runner/model.test.ts +++ b/src/agents/embedded-agent-runner/model.test.ts @@ -2,11 +2,11 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { discoverAuthStorage, discoverModels } from "../agent-model-discovery.js"; import { clearRuntimeAuthProfileStoreSnapshots, replaceRuntimeAuthProfileStoreSnapshots, } from "../auth-profiles.js"; -import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js"; import { resetModelDiscoveryCacheForTest } from "./model-discovery-cache.js"; import { createProviderRuntimeTestMock } from "./model.provider-runtime.test-support.js"; @@ -118,7 +118,7 @@ vi.mock("../model-suppression.js", () => { }; }); -vi.mock("../pi-model-discovery.js", () => ({ +vi.mock("../agent-model-discovery.js", () => ({ discoverAuthStorage: vi.fn(() => ({ mocked: true })), discoverModels: vi.fn(() => ({ find: vi.fn(() => null) })), })); @@ -269,7 +269,7 @@ function mockCallArg(mock: ReturnType, callIndex = 0): Record { - it("reuses PI discovery stores while the agent model files are unchanged", async () => { + it("reuses agent discovery stores while the agent model files are unchanged", async () => { mockDiscoveredModel(discoverModels, { provider: "openai", modelId: "gpt-5.5", @@ -292,7 +292,7 @@ describe("resolveModel", () => { expect(discoverModels).toHaveBeenCalledTimes(1); }); - it("invalidates PI discovery stores when inherited default auth changes", async () => { + it("invalidates agent discovery stores when inherited default auth changes", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-model-cache-")); const agentDir = path.join(rootDir, "agent"); const defaultAgentDir = path.join(rootDir, "default-agent"); @@ -332,7 +332,7 @@ describe("resolveModel", () => { expect(discoverModels).toHaveBeenCalledTimes(2); }); - it("invalidates PI discovery stores when implicit main auth changes without config", async () => { + it("invalidates agent discovery stores when implicit main auth changes without config", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-model-cache-state-")); vi.stubEnv("OPENCLAW_STATE_DIR", rootDir); const agentDir = path.join(rootDir, "agents", "worker", "agent"); @@ -365,7 +365,7 @@ describe("resolveModel", () => { expect(discoverModels).toHaveBeenCalledTimes(2); }); - it("does not cache PI discovery stores while runtime auth snapshots are active", async () => { + it("does not cache agent discovery stores while runtime auth snapshots are active", async () => { replaceRuntimeAuthProfileStoreSnapshots([ { store: { @@ -398,7 +398,7 @@ describe("resolveModel", () => { expect(discoverModels).toHaveBeenCalledTimes(2); }); - it("does not cache PI discovery stores while plugin auth overlays are active", async () => { + it("does not cache agent discovery stores while plugin auth overlays are active", async () => { resolveRuntimeSyntheticAuthProviderRefsMock.mockReturnValue(["runtime-provider"]); resolveRuntimeExternalAuthProviderRefsMock.mockReturnValue(["external-provider"]); mockDiscoveredModel(discoverModels, { @@ -423,7 +423,7 @@ describe("resolveModel", () => { expect(discoverModels).toHaveBeenCalledTimes(2); }); - it("skips PI auth and model discovery during dynamic model resolution", async () => { + it("skips OpenClaw auth and model discovery during dynamic model resolution", async () => { const result = await resolveModelAsync( "openrouter", "openrouter/auto", @@ -431,7 +431,7 @@ describe("resolveModel", () => { undefined, { runtimeHooks: createRuntimeHooks(), - skipPiDiscovery: true, + skipAgentDiscovery: true, }, ); @@ -443,7 +443,7 @@ describe("resolveModel", () => { expect(discoverModels).not.toHaveBeenCalled(); }); - it("resolves opt-in bundled static catalog rows while skipping PI discovery", async () => { + it("resolves opt-in bundled static catalog rows while skipping agent discovery", async () => { resolveBundledStaticCatalogModelMock.mockReturnValueOnce({ provider: "mistral", id: "mistral-medium-3-5", @@ -465,7 +465,7 @@ describe("resolveModel", () => { { allowBundledStaticCatalogFallback: true, runtimeHooks: createRuntimeHooks(), - skipPiDiscovery: true, + skipAgentDiscovery: true, }, ); @@ -488,7 +488,7 @@ describe("resolveModel", () => { expect(discoverModels).not.toHaveBeenCalled(); }); - it("applies provider overrides to bundled static catalog rows while skipping PI discovery", async () => { + it("applies provider overrides to bundled static catalog rows while skipping agent discovery", async () => { resolveBundledStaticCatalogModelMock.mockReturnValueOnce({ provider: "mistral", id: "mistral-medium-3-5", @@ -524,7 +524,7 @@ describe("resolveModel", () => { const result = await resolveModelAsync("mistral", "mistral-medium-3-5", "/tmp/agent", cfg, { allowBundledStaticCatalogFallback: true, runtimeHooks: createRuntimeHooks(), - skipPiDiscovery: true, + skipAgentDiscovery: true, }); const model = expectResolvedModel(result); @@ -645,7 +645,7 @@ describe("resolveModel", () => { undefined, { runtimeHooks: createRuntimeHooks(), - skipPiDiscovery: true, + skipAgentDiscovery: true, }, ); @@ -690,7 +690,7 @@ describe("resolveModel", () => { expect(expectResolvedModel(result).input).toEqual(["text"]); }); - it("defaults missing model cost before handing models to PI", () => { + it("defaults missing model cost before handing models to OpenClaw", () => { const cfg: OpenClawConfig = { models: { providers: { @@ -1101,7 +1101,7 @@ describe("resolveModel", () => { }); }); - it("adds GitHub Copilot IDE headers to dynamic resolved model headers for Pi-native compaction", () => { + it("adds GitHub Copilot IDE headers to dynamic resolved model headers for native compaction", () => { const result = resolveModelForTest("github-copilot", "gpt-5.5", "/tmp/agent"); const model = expectResolvedModel(result) as unknown as { headers?: Record }; @@ -1112,7 +1112,7 @@ describe("resolveModel", () => { }); }); - it("adds GitHub Copilot IDE headers to configured resolved model headers for Pi-native compaction", () => { + it("adds GitHub Copilot IDE headers to configured resolved model headers for native compaction", () => { const cfg = { models: { providers: { @@ -1770,7 +1770,7 @@ describe("resolveModel", () => { const result = await resolveModelAsync("microsoft-foundry", "Kimi-K2.6-1", "/tmp/agent", cfg, { runtimeHooks: createRuntimeHooks(), - skipPiDiscovery: true, + skipAgentDiscovery: true, }); expect(result.error).toBe( diff --git a/src/agents/pi-embedded-runner/model.ts b/src/agents/embedded-agent-runner/model.ts similarity index 94% rename from src/agents/pi-embedded-runner/model.ts rename to src/agents/embedded-agent-runner/model.ts index 9c6b2305ac3..707323a2b12 100644 --- a/src/agents/pi-embedded-runner/model.ts +++ b/src/agents/embedded-agent-runner/model.ts @@ -1,11 +1,4 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; -import { - AuthStorage as PiAuthStorageClass, - ModelRegistry as PiModelRegistryClass, - type AuthStorage, - type ModelRegistry, -} from "@earendil-works/pi-coding-agent"; -import type { ModelMediaInputConfig } from "../../config/types.models.js"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js"; import { @@ -18,6 +11,7 @@ import { normalizeProviderResolvedModelWithPlugin, shouldPreferProviderRuntimeResolvedModel, } from "../../plugins/provider-runtime.js"; +import { discoverAuthStorage, discoverModels } from "../agent-model-discovery.js"; import { resolveDefaultAgentDir } from "../agent-scope.js"; import { DEFAULT_CONTEXT_TOKENS } from "../defaults.js"; import { buildModelAliasLines } from "../model-alias-lines.js"; @@ -28,14 +22,19 @@ import { shouldSuppressBuiltInModel, shouldUnconditionallySuppress, } from "../model-suppression.js"; -import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js"; import { attachModelProviderLocalService } from "../provider-local-service.js"; import { attachModelProviderRequestTransport, resolveProviderRequestConfig, sanitizeConfiguredModelProviderRequest, } from "../provider-request-config.js"; -import { discoverCachedPiStores } from "./model-discovery-cache.js"; +import { + AuthStorage as AgentAuthStorageClass, + ModelRegistry as AgentModelRegistryClass, + type AuthStorage, + type ModelRegistry, +} from "../sessions/index.js"; +import { discoverCachedAgentStores } from "./model-discovery-cache.js"; import { buildInlineProviderModels, type InlineProviderConfig, @@ -99,30 +98,30 @@ const STATIC_PROVIDER_RUNTIME_HOOKS: ProviderRuntimeHooks = { normalizeProviderTransportWithPlugin: () => undefined, }; -const SKIP_PI_DISCOVERY_PROVIDER_RUNTIME_HOOKS: ProviderRuntimeHooks = { - // skipPiDiscovery is the lean path used before PI discovery/models.json has run. +const SKIP_AGENT_DISCOVERY_PROVIDER_RUNTIME_HOOKS: ProviderRuntimeHooks = { + // skipAgentDiscovery is the lean path used before agent discovery/models.json has run. ...TARGET_PROVIDER_RUNTIME_HOOKS, }; -function createEmptyPiDiscoveryStores(): { +function createEmptyAgentDiscoveryStores(): { authStorage: AuthStorage; modelRegistry: ModelRegistry; } { const authStorage = - typeof PiAuthStorageClass.inMemory === "function" - ? PiAuthStorageClass.inMemory({}) - : PiAuthStorageClass.create(); + typeof AgentAuthStorageClass.inMemory === "function" + ? AgentAuthStorageClass.inMemory({}) + : AgentAuthStorageClass.create(); const modelRegistry = - typeof PiModelRegistryClass.inMemory === "function" - ? PiModelRegistryClass.inMemory(authStorage) - : PiModelRegistryClass.create(authStorage); + typeof AgentModelRegistryClass.inMemory === "function" + ? AgentModelRegistryClass.inMemory(authStorage) + : AgentModelRegistryClass.create(authStorage); return { authStorage, modelRegistry }; } function resolveRuntimeHooks(params?: { runtimeHooks?: ProviderRuntimeHooks; skipProviderRuntimeHooks?: boolean; - skipPiDiscovery?: boolean; + skipAgentDiscovery?: boolean; }): ProviderRuntimeHooks { if (params?.skipProviderRuntimeHooks) { return STATIC_PROVIDER_RUNTIME_HOOKS; @@ -130,29 +129,26 @@ function resolveRuntimeHooks(params?: { if (params?.runtimeHooks) { return params.runtimeHooks; } - if (params?.skipPiDiscovery) { - return SKIP_PI_DISCOVERY_PROVIDER_RUNTIME_HOOKS; + if (params?.skipAgentDiscovery) { + return SKIP_AGENT_DISCOVERY_PROVIDER_RUNTIME_HOOKS; } return DEFAULT_PROVIDER_RUNTIME_HOOKS; } -function discoverCachedPiStoresForAgent( +function discoverCachedAgentStoresForAgent( resolvedAgentDir: string, cfg: OpenClawConfig | undefined, ): { authStorage: AuthStorage; modelRegistry: ModelRegistry; } { - return discoverCachedPiStores({ + return discoverCachedAgentStores({ agentDir: resolvedAgentDir, inheritedAuthDir: resolveDefaultAgentDir(cfg ?? {}), }); } -function canonicalizeLegacyResolvedModel(params: { - provider: string; - model: Model; -}): Model { +function canonicalizeLegacyResolvedModel(params: { provider: string; model: Model }): Model { if ( normalizeProviderId(params.provider) !== "openai-codex" || params.model.id.trim().toLowerCase() !== "gpt-5.4-codex" @@ -172,8 +168,8 @@ function applyResolvedTransportFallback(params: { cfg?: OpenClawConfig; workspaceDir?: string; runtimeHooks: ProviderRuntimeHooks; - model: Model; -}): Model | undefined { + model: Model; +}): Model | undefined { const normalized = params.runtimeHooks.normalizeProviderTransportWithPlugin({ provider: params.provider, config: params.cfg, @@ -203,17 +199,17 @@ function applyResolvedTransportFallback(params: { function normalizeResolvedModel(params: { provider: string; - model: Model; + model: Model; cfg?: OpenClawConfig; agentDir?: string; workspaceDir?: string; runtimeHooks?: ProviderRuntimeHooks; -}): Model { - const normalizeModelCost = (cost: unknown): Model["cost"] => { +}): Model { + const normalizeModelCost = (cost: unknown): Model["cost"] => { if (!cost || typeof cost !== "object" || Array.isArray(cost)) { return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; } - const record = cost as Partial["cost"]>; + const record = cost as Partial; const input = typeof record.input === "number" && Number.isFinite(record.input) ? record.input : 0; const output = @@ -232,7 +228,7 @@ function normalizeResolvedModel(params: { cacheRead === record.cacheRead && cacheWrite === record.cacheWrite ) { - return record as Model["cost"]; + return record as Model["cost"]; } return { ...cost, @@ -252,7 +248,7 @@ function normalizeResolvedModel(params: { input: params.model.input, }), cost: normalizeModelCost((params.model as { cost?: unknown }).cost), - } as Model; + } as Model; const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS; const pluginNormalized = runtimeHooks.normalizeProviderResolvedModelWithPlugin({ provider: params.provider, @@ -266,7 +262,7 @@ function normalizeResolvedModel(params: { modelId: normalizedInputModel.id, model: normalizedInputModel, }, - }) as Model | undefined; + }) as Model | undefined; const compatNormalized = runtimeHooks.applyProviderResolvedModelCompatWithPlugins?.({ provider: params.provider, config: params.cfg, @@ -279,7 +275,7 @@ function normalizeResolvedModel(params: { modelId: normalizedInputModel.id, model: (pluginNormalized ?? normalizedInputModel) as never, }, - }) as Model | undefined; + }) as Model | undefined; const transportNormalized = runtimeHooks.applyProviderResolvedTransportWithPlugin?.({ provider: params.provider, config: params.cfg, @@ -292,7 +288,7 @@ function normalizeResolvedModel(params: { modelId: normalizedInputModel.id, model: (compatNormalized ?? pluginNormalized ?? normalizedInputModel) as never, }, - }) as Model | undefined; + }) as Model | undefined; const fallbackTransportNormalized = transportNormalized ?? applyResolvedTransportFallback({ @@ -744,7 +740,7 @@ function resolveExplicitModelWithRegistry(params: { agentDir?: string; workspaceDir?: string; runtimeHooks?: ProviderRuntimeHooks; -}): { kind: "resolved"; model: Model } | { kind: "suppressed" } | undefined { +}): { kind: "resolved"; model: Model } | { kind: "suppressed" } | undefined { const { provider, modelId, modelRegistry, cfg, agentDir, workspaceDir, runtimeHooks } = params; const providerConfig = resolveConfiguredProviderConfig(cfg, provider); const requestTimeoutMs = resolveProviderRequestTimeoutMs(providerConfig?.timeoutSeconds); @@ -780,7 +776,7 @@ function resolveExplicitModelWithRegistry(params: { ...inlineMatch, ...(resolvedParams ? { params: resolvedParams } : {}), ...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}), - } as Model, + } as Model, runtimeHooks, }), }; @@ -795,7 +791,7 @@ function resolveExplicitModelWithRegistry(params: { ) { return { kind: "suppressed" }; } - const model = modelRegistry.find(provider, modelId) as Model | null; + const model = modelRegistry.find(provider, modelId) as Model | null; if (model) { return { @@ -844,7 +840,7 @@ function resolveExplicitModelWithRegistry(params: { ...fallbackInlineMatch, ...(resolvedParams ? { params: resolvedParams } : {}), ...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}), - } as Model, + } as Model, runtimeHooks, }), }; @@ -861,7 +857,7 @@ function resolvePluginDynamicModelWithRegistry(params: { agentDir?: string; workspaceDir?: string; runtimeHooks?: ProviderRuntimeHooks; -}): Model | undefined { +}): Model | undefined { const { provider, modelId, modelRegistry, cfg, agentDir, workspaceDir } = params; const runtimeHooks = params.runtimeHooks ?? DEFAULT_PROVIDER_RUNTIME_HOOKS; const providerConfig = resolveConfiguredProviderConfig(cfg, provider); @@ -886,7 +882,7 @@ function resolvePluginDynamicModelWithRegistry(params: { modelRegistry, providerConfig, }, - }) as Model | undefined; + }) as Model | undefined; if (!pluginDynamicModel) { return undefined; } @@ -917,7 +913,7 @@ function resolveConfiguredFallbackModel(params: { agentDir?: string; workspaceDir?: string; runtimeHooks?: ProviderRuntimeHooks; -}): Model | undefined { +}): Model | undefined { const { provider, modelId, cfg, agentDir, workspaceDir, runtimeHooks } = params; const providerConfig = resolveConfiguredProviderConfig(cfg, provider); const requestTimeoutMs = resolveProviderRequestTimeoutMs(providerConfig?.timeoutSeconds); @@ -1000,7 +996,7 @@ function resolveConfiguredFallbackModel(params: { ...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}), headers: requestConfig.headers, mediaInput: configuredModel?.mediaInput, - } as Model, + } as Model, providerRequest, ), providerConfig?.localService, @@ -1034,9 +1030,9 @@ function shouldCompareProviderRuntimeResolvedModel(params: { } function preferProviderRuntimeResolvedModel(params: { - explicitModel: Model; - runtimeResolvedModel?: Model; -}): Model { + explicitModel: Model; + runtimeResolvedModel?: Model; +}): Model { if (params.runtimeResolvedModel) { return params.runtimeResolvedModel; } @@ -1051,7 +1047,7 @@ export function resolveModelWithRegistry(params: { agentDir?: string; workspaceDir?: string; runtimeHooks?: ProviderRuntimeHooks; -}): Model | undefined { +}): Model | undefined { const normalizedRef = { provider: params.provider, model: normalizeStaticProviderModelId(normalizeProviderId(params.provider), params.modelId), @@ -1112,7 +1108,7 @@ export function resolveModel( workspaceDir?: string; }, ): { - model?: Model; + model?: Model; error?: string; authStorage: AuthStorage; modelRegistry: ModelRegistry; @@ -1125,7 +1121,7 @@ export function resolveModel( const workspaceDir = options?.workspaceDir ?? cfg?.agents?.defaults?.workspace; const cachedStores = !options?.authStorage && !options?.modelRegistry - ? discoverCachedPiStoresForAgent(resolvedAgentDir, cfg) + ? discoverCachedAgentStoresForAgent(resolvedAgentDir, cfg) : undefined; const authStorage = options?.authStorage ?? cachedStores?.authStorage ?? discoverAuthStorage(resolvedAgentDir); @@ -1173,11 +1169,11 @@ export async function resolveModelAsync( retryTransientProviderRuntimeMiss?: boolean; runtimeHooks?: ProviderRuntimeHooks; skipProviderRuntimeHooks?: boolean; - skipPiDiscovery?: boolean; + skipAgentDiscovery?: boolean; workspaceDir?: string; }, ): Promise<{ - model?: Model; + model?: Model; error?: string; authStorage: AuthStorage; modelRegistry: ModelRegistry; @@ -1189,12 +1185,12 @@ export async function resolveModelAsync( const resolvedAgentDir = agentDir ?? resolveDefaultAgentDir(cfg ?? {}); const workspaceDir = options?.workspaceDir ?? cfg?.agents?.defaults?.workspace; const emptyDiscoveryStores = - options?.skipPiDiscovery && (!options.authStorage || !options.modelRegistry) - ? createEmptyPiDiscoveryStores() + options?.skipAgentDiscovery && (!options.authStorage || !options.modelRegistry) + ? createEmptyAgentDiscoveryStores() : undefined; const cachedStores = !emptyDiscoveryStores && !options?.authStorage && !options?.modelRegistry - ? discoverCachedPiStoresForAgent(resolvedAgentDir, cfg) + ? discoverCachedAgentStoresForAgent(resolvedAgentDir, cfg) : undefined; const authStorage = options?.authStorage ?? diff --git a/src/agents/pi-embedded-runner/openrouter-model-capabilities.test.ts b/src/agents/embedded-agent-runner/openrouter-model-capabilities.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/openrouter-model-capabilities.test.ts rename to src/agents/embedded-agent-runner/openrouter-model-capabilities.test.ts diff --git a/src/agents/pi-embedded-runner/openrouter-model-capabilities.ts b/src/agents/embedded-agent-runner/openrouter-model-capabilities.ts similarity index 100% rename from src/agents/pi-embedded-runner/openrouter-model-capabilities.ts rename to src/agents/embedded-agent-runner/openrouter-model-capabilities.ts diff --git a/src/agents/pi-embedded-runner/post-compaction-loop-guard.test.ts b/src/agents/embedded-agent-runner/post-compaction-loop-guard.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/post-compaction-loop-guard.test.ts rename to src/agents/embedded-agent-runner/post-compaction-loop-guard.test.ts diff --git a/src/agents/pi-embedded-runner/post-compaction-loop-guard.ts b/src/agents/embedded-agent-runner/post-compaction-loop-guard.ts similarity index 100% rename from src/agents/pi-embedded-runner/post-compaction-loop-guard.ts rename to src/agents/embedded-agent-runner/post-compaction-loop-guard.ts diff --git a/src/agents/pi-embedded-runner/prompt-cache-observability.test.ts b/src/agents/embedded-agent-runner/prompt-cache-observability.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/prompt-cache-observability.test.ts rename to src/agents/embedded-agent-runner/prompt-cache-observability.test.ts diff --git a/src/agents/pi-embedded-runner/prompt-cache-observability.ts b/src/agents/embedded-agent-runner/prompt-cache-observability.ts similarity index 100% rename from src/agents/pi-embedded-runner/prompt-cache-observability.ts rename to src/agents/embedded-agent-runner/prompt-cache-observability.ts diff --git a/src/agents/pi-embedded-runner/prompt-cache-retention.test.ts b/src/agents/embedded-agent-runner/prompt-cache-retention.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/prompt-cache-retention.test.ts rename to src/agents/embedded-agent-runner/prompt-cache-retention.test.ts diff --git a/src/agents/pi-embedded-runner/prompt-cache-retention.ts b/src/agents/embedded-agent-runner/prompt-cache-retention.ts similarity index 91% rename from src/agents/pi-embedded-runner/prompt-cache-retention.ts rename to src/agents/embedded-agent-runner/prompt-cache-retention.ts index 5b5c0d5357d..a50de7e3ab5 100644 --- a/src/agents/pi-embedded-runner/prompt-cache-retention.ts +++ b/src/agents/embedded-agent-runner/prompt-cache-retention.ts @@ -1,5 +1,5 @@ +import { resolveAnthropicCacheRetentionFamily } from "../../llm/providers/stream-wrappers/anthropic-family-cache-semantics.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; -import { resolveAnthropicCacheRetentionFamily } from "./anthropic-family-cache-semantics.js"; type CacheRetention = "none" | "short" | "long"; diff --git a/src/agents/pi-embedded-runner/replay-history.test.ts b/src/agents/embedded-agent-runner/replay-history.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/replay-history.test.ts rename to src/agents/embedded-agent-runner/replay-history.test.ts index cd1f8cf6eff..31cc68a0df5 100644 --- a/src/agents/pi-embedded-runner/replay-history.test.ts +++ b/src/agents/embedded-agent-runner/replay-history.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; import { INTERNAL_RUNTIME_CONTEXT_BEGIN, diff --git a/src/agents/pi-embedded-runner/replay-history.ts b/src/agents/embedded-agent-runner/replay-history.ts similarity index 98% rename from src/agents/pi-embedded-runner/replay-history.ts rename to src/agents/embedded-agent-runner/replay-history.ts index 65ade368671..9292901ff7a 100644 --- a/src/agents/pi-embedded-runner/replay-history.ts +++ b/src/agents/embedded-agent-runner/replay-history.ts @@ -1,5 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { SessionManager } from "@earendil-works/pi-coding-agent"; import { stripInternalMetadataForDisplay } from "../../auto-reply/reply/display-text-sanitize.js"; import { isSilentReplyPayloadText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; @@ -17,8 +15,6 @@ import { hasInterSessionUserProvenance, normalizeInputProvenance, } from "../../sessions/input-provenance.js"; -import { asFiniteNumber } from "../../shared/number-coercion.js"; -import { resolveImageSanitizationLimits } from "../image-sanitization.js"; import { downgradeOpenAIFunctionCallReasoningPairs, downgradeOpenAIReasoningBlocks, @@ -26,12 +22,15 @@ import { sanitizeSessionMessagesImages, validateAnthropicTurns, validateGeminiTurns, -} from "../pi-embedded-helpers.js"; +} from "../embedded-agent-helpers.js"; +import { resolveImageSanitizationLimits } from "../image-sanitization.js"; +import type { AgentMessage } from "../runtime/index.js"; import { sanitizeToolCallInputs, sanitizeToolUseResultPairing, stripToolResultDetails, } from "../session-transcript-repair.js"; +import type { SessionManager } from "../sessions/index.js"; import { STREAM_ERROR_FALLBACK_TEXT } from "../stream-message-shared.js"; import { sanitizeToolCallIdsForCloudCodeAssist } from "../tool-call-id.js"; import type { TranscriptPolicy } from "../transcript-policy.js"; @@ -218,7 +217,7 @@ function stripStaleAssistantUsageBeforeLatestCompaction(messages: AgentMessage[] continue; } - // pi-coding-agent expects assistant usage to always be present during context + // session runtime expects assistant usage to always be present during context // accounting. Keep stale snapshots structurally valid, but zeroed out. const candidateRecord = candidate as unknown as Record; out[i] = { @@ -537,7 +536,7 @@ function normalizeAssistantUsageCost(usage: unknown): AssistantUsageSnapshot["co } function toFiniteCostNumber(value: unknown): number | undefined { - return asFiniteNumber(value); + return typeof value === "number" && Number.isFinite(value) ? value : undefined; } function ensureAssistantUsageSnapshots(messages: AgentMessage[]): AgentMessage[] { diff --git a/src/agents/pi-embedded-runner/replay-state.ts b/src/agents/embedded-agent-runner/replay-state.ts similarity index 100% rename from src/agents/pi-embedded-runner/replay-state.ts rename to src/agents/embedded-agent-runner/replay-state.ts diff --git a/src/agents/pi-embedded-runner/resource-loader.test.ts b/src/agents/embedded-agent-runner/resource-loader.test.ts similarity index 63% rename from src/agents/pi-embedded-runner/resource-loader.test.ts rename to src/agents/embedded-agent-runner/resource-loader.test.ts index 242596b9524..e37403b3e8e 100644 --- a/src/agents/pi-embedded-runner/resource-loader.test.ts +++ b/src/agents/embedded-agent-runner/resource-loader.test.ts @@ -1,11 +1,11 @@ -import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent"; import { describe, expect, it, vi } from "vitest"; +import { DefaultResourceLoader } from "../sessions/index.js"; import { - createEmbeddedPiResourceLoader, - EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS, + createEmbeddedAgentResourceLoader, + EMBEDDED_AGENT_RESOURCE_LOADER_DISCOVERY_OPTIONS, } from "./resource-loader.js"; -vi.mock("@earendil-works/pi-coding-agent", () => ({ +vi.mock("../sessions/index.js", () => ({ DefaultResourceLoader: vi.fn(function DefaultResourceLoader( this: Record, options: unknown, @@ -17,12 +17,12 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({ }), })); -describe("createEmbeddedPiResourceLoader", () => { - it("keeps inline extensions but disables Pi filesystem discovery", () => { +describe("createEmbeddedAgentResourceLoader", () => { + it("keeps inline extensions but disables filesystem discovery", () => { const settingsManager = {}; const extensionFactories = [vi.fn()]; - createEmbeddedPiResourceLoader({ + createEmbeddedAgentResourceLoader({ cwd: "/workspace", agentDir: "/agent", settingsManager: settingsManager as never, @@ -34,7 +34,7 @@ describe("createEmbeddedPiResourceLoader", () => { agentDir: "/agent", settingsManager, extensionFactories, - ...EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS, + ...EMBEDDED_AGENT_RESOURCE_LOADER_DISCOVERY_OPTIONS, }); }); }); diff --git a/src/agents/pi-embedded-runner/resource-loader.ts b/src/agents/embedded-agent-runner/resource-loader.ts similarity index 65% rename from src/agents/pi-embedded-runner/resource-loader.ts rename to src/agents/embedded-agent-runner/resource-loader.ts index 0f122d21792..f41abbc528a 100644 --- a/src/agents/pi-embedded-runner/resource-loader.ts +++ b/src/agents/embedded-agent-runner/resource-loader.ts @@ -1,8 +1,8 @@ -import { DefaultResourceLoader } from "@earendil-works/pi-coding-agent"; +import { DefaultResourceLoader } from "../sessions/index.js"; type DefaultResourceLoaderInit = ConstructorParameters[0]; -export const EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS = { +export const EMBEDDED_AGENT_RESOURCE_LOADER_DISCOVERY_OPTIONS = { noExtensions: true, noSkills: true, noPromptTemplates: true, @@ -10,7 +10,7 @@ export const EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS = { noContextFiles: true, } satisfies Partial; -export function createEmbeddedPiResourceLoader( +export function createEmbeddedAgentResourceLoader( options: Pick< DefaultResourceLoaderInit, "cwd" | "agentDir" | "settingsManager" | "extensionFactories" @@ -18,6 +18,6 @@ export function createEmbeddedPiResourceLoader( ): DefaultResourceLoader { return new DefaultResourceLoader({ ...options, - ...EMBEDDED_PI_RESOURCE_LOADER_DISCOVERY_OPTIONS, + ...EMBEDDED_AGENT_RESOURCE_LOADER_DISCOVERY_OPTIONS, }); } diff --git a/src/agents/pi-embedded-runner/result-fallback-classifier.test.ts b/src/agents/embedded-agent-runner/result-fallback-classifier.test.ts similarity index 68% rename from src/agents/pi-embedded-runner/result-fallback-classifier.test.ts rename to src/agents/embedded-agent-runner/result-fallback-classifier.test.ts index d56569cbb91..98015d9d113 100644 --- a/src/agents/pi-embedded-runner/result-fallback-classifier.test.ts +++ b/src/agents/embedded-agent-runner/result-fallback-classifier.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest"; -import { classifyEmbeddedPiRunResultForModelFallback } from "./result-fallback-classifier.js"; +import { classifyEmbeddedAgentRunResultForModelFallback } from "./result-fallback-classifier.js"; -describe("classifyEmbeddedPiRunResultForModelFallback", () => { +describe("classifyEmbeddedAgentRunResultForModelFallback", () => { it("does not fallback when sessions_spawn accepted a child session", () => { expect( - classifyEmbeddedPiRunResultForModelFallback({ + classifyEmbeddedAgentRunResultForModelFallback({ provider: "mock-openai", model: "gpt-5.5", result: { diff --git a/src/agents/pi-embedded-runner/result-fallback-classifier.ts b/src/agents/embedded-agent-runner/result-fallback-classifier.ts similarity index 91% rename from src/agents/pi-embedded-runner/result-fallback-classifier.ts rename to src/agents/embedded-agent-runner/result-fallback-classifier.ts index 124787dc213..fdecae2352e 100644 --- a/src/agents/pi-embedded-runner/result-fallback-classifier.ts +++ b/src/agents/embedded-agent-runner/result-fallback-classifier.ts @@ -2,12 +2,12 @@ import { isSilentReplyPayloadText } from "../../auto-reply/tokens.js"; import { isGpt5ModelId } from "../gpt5-prompt-overlay.js"; import type { ModelFallbackResultClassification } from "../model-fallback.js"; import { hasOutboundDeliveryEvidence, hasVisibleAgentPayload } from "./delivery-evidence.js"; -import type { EmbeddedPiRunResult } from "./types.js"; +import type { EmbeddedAgentRunResult } from "./types.js"; const EMPTY_TERMINAL_REPLY_RE = /Agent couldn't generate a response/i; const PLAN_ONLY_TERMINAL_REPLY_RE = /Agent stopped after repeated plan-only turns/i; -function isEmbeddedPiRunResult(value: unknown): value is EmbeddedPiRunResult { +function isEmbeddedAgentRunResult(value: unknown): value is EmbeddedAgentRunResult { return Boolean( value && typeof value === "object" && @@ -17,7 +17,7 @@ function isEmbeddedPiRunResult(value: unknown): value is EmbeddedPiRunResult { ); } -function hasDeliberateSilentTerminalReply(result: EmbeddedPiRunResult): boolean { +function hasDeliberateSilentTerminalReply(result: EmbeddedAgentRunResult): boolean { if (result.meta.error?.kind === "hook_block") { return true; } @@ -29,7 +29,7 @@ function hasDeliberateSilentTerminalReply(result: EmbeddedPiRunResult): boolean function classifyHarnessResult(params: { provider: string; model: string; - result: EmbeddedPiRunResult; + result: EmbeddedAgentRunResult; }): ModelFallbackResultClassification { switch (params.result.meta.agentHarnessResultClassification) { case "empty": @@ -55,14 +55,14 @@ function classifyHarnessResult(params: { } } -export function classifyEmbeddedPiRunResultForModelFallback(params: { +export function classifyEmbeddedAgentRunResultForModelFallback(params: { provider: string; model: string; result: unknown; hasDirectlySentBlockReply?: boolean; hasBlockReplyPipelineOutput?: boolean; }): ModelFallbackResultClassification { - if (!isEmbeddedPiRunResult(params.result)) { + if (!isEmbeddedAgentRunResult(params.result)) { return null; } if ( diff --git a/src/agents/pi-embedded-runner/run-state.ts b/src/agents/embedded-agent-runner/run-state.ts similarity index 91% rename from src/agents/pi-embedded-runner/run-state.ts rename to src/agents/embedded-agent-runner/run-state.ts index 28553cf2de2..1c29d865927 100644 --- a/src/agents/pi-embedded-runner/run-state.ts +++ b/src/agents/embedded-agent-runner/run-state.ts @@ -6,9 +6,9 @@ import { } from "../../auto-reply/reply/reply-run-registry.js"; import { resolveGlobalSingleton } from "../../shared/global-singleton.js"; -export type EmbeddedPiQueueHandle = { +export type EmbeddedAgentQueueHandle = { kind?: "embedded"; - queueMessage: (text: string, options?: EmbeddedPiQueueMessageOptions) => Promise; + queueMessage: (text: string, options?: EmbeddedAgentQueueMessageOptions) => Promise; isStreaming: () => boolean; isCompacting: () => boolean; supportsTranscriptCommitWait?: boolean; @@ -17,7 +17,7 @@ export type EmbeddedPiQueueHandle = { sourceReplyDeliveryMode?: SourceReplyDeliveryMode; }; -export type EmbeddedPiQueueMessageOptions = { +export type EmbeddedAgentQueueMessageOptions = { steeringMode?: "all"; debounceMs?: number; deliveryTimeoutMs?: number; @@ -46,7 +46,7 @@ export type EmbeddedRunWaiter = { const EMBEDDED_RUN_STATE_KEY = Symbol.for("openclaw.embeddedRunState"); const embeddedRunState = resolveGlobalSingleton(EMBEDDED_RUN_STATE_KEY, () => ({ - activeRuns: new Map(), + activeRuns: new Map(), snapshots: new Map(), sessionIdsByKey: new Map(), sessionIdsByFile: new Map(), @@ -56,7 +56,7 @@ const embeddedRunState = resolveGlobalSingleton(EMBEDDED_RUN_STATE_KEY, () => ({ export const ACTIVE_EMBEDDED_RUNS = embeddedRunState.activeRuns ?? - (embeddedRunState.activeRuns = new Map()); + (embeddedRunState.activeRuns = new Map()); export const ACTIVE_EMBEDDED_RUN_SNAPSHOTS = embeddedRunState.snapshots ?? (embeddedRunState.snapshots = new Map()); diff --git a/src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts b/src/agents/embedded-agent-runner/run.before-agent-reply-cron.test.ts similarity index 92% rename from src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts rename to src/agents/embedded-agent-runner/run.before-agent-reply-cron.test.ts index 31b7d8488e4..356a9e26bf2 100644 --- a/src/agents/pi-embedded-runner/run.before-agent-reply-cron.test.ts +++ b/src/agents/embedded-agent-runner/run.before-agent-reply-cron.test.ts @@ -9,7 +9,7 @@ import { resetRunOverflowCompactionHarnessMocks, } from "./run.overflow-compaction.harness.js"; -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; function firstBeforeAgentReplyCall() { const call = mockedGlobalHookRunner.runBeforeAgentReply.mock.calls[0]; @@ -29,9 +29,9 @@ function firstAttemptParams(): { modelRun?: boolean; promptMode?: string } { return call[0]; } -describe("runEmbeddedPiAgent cron before_agent_reply seam", () => { +describe("runEmbeddedAgent cron before_agent_reply seam", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); }); beforeEach(() => { @@ -48,7 +48,7 @@ describe("runEmbeddedPiAgent cron before_agent_reply seam", () => { }); const onExecutionPhase = vi.fn(); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, trigger: "cron", jobId: "cron-job-123", @@ -82,7 +82,7 @@ describe("runEmbeddedPiAgent cron before_agent_reply seam", () => { handled: true, }); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, trigger: "cron", }); @@ -99,7 +99,7 @@ describe("runEmbeddedPiAgent cron before_agent_reply seam", () => { mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); const onExecutionPhase = vi.fn(); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, trigger: "cron", onExecutionPhase, @@ -120,7 +120,7 @@ describe("runEmbeddedPiAgent cron before_agent_reply seam", () => { ); mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, trigger: "user", }); @@ -132,7 +132,7 @@ describe("runEmbeddedPiAgent cron before_agent_reply seam", () => { it("forwards one-shot model-run flags into the embedded attempt", async () => { mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, trigger: "user", modelRun: true, diff --git a/src/agents/pi-embedded-runner/run.codex-app-server-recovery.test.ts b/src/agents/embedded-agent-runner/run.codex-app-server-recovery.test.ts similarity index 94% rename from src/agents/pi-embedded-runner/run.codex-app-server-recovery.test.ts rename to src/agents/embedded-agent-runner/run.codex-app-server-recovery.test.ts index 12183d6d540..ee005d94159 100644 --- a/src/agents/pi-embedded-runner/run.codex-app-server-recovery.test.ts +++ b/src/agents/embedded-agent-runner/run.codex-app-server-recovery.test.ts @@ -12,7 +12,7 @@ import { } from "./run.overflow-compaction.harness.js"; import type { EmbeddedRunAttemptResult } from "./run/types.js"; -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; function codexClientClosedAttempt( overrides: Partial = {}, @@ -39,9 +39,9 @@ function successAttempt(): EmbeddedRunAttemptResult { }); } -describe("runEmbeddedPiAgent Codex app-server recovery", () => { +describe("runEmbeddedAgent Codex app-server recovery", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); }); beforeEach(() => { @@ -54,7 +54,7 @@ describe("runEmbeddedPiAgent Codex app-server recovery", () => { .mockResolvedValueOnce(codexClientClosedAttempt()) .mockResolvedValueOnce(successAttempt()); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "codex", model: "gpt-5.5", @@ -69,7 +69,7 @@ describe("runEmbeddedPiAgent Codex app-server recovery", () => { .mockResolvedValueOnce(codexClientClosedAttempt()) .mockResolvedValueOnce(successAttempt()); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "codex", model: "gpt-5.5", @@ -97,7 +97,7 @@ describe("runEmbeddedPiAgent Codex app-server recovery", () => { }) .mockResolvedValueOnce(successAttempt()); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "codex", model: "gpt-5.5", @@ -128,7 +128,7 @@ describe("runEmbeddedPiAgent Codex app-server recovery", () => { ); await expect( - runEmbeddedPiAgent({ + runEmbeddedAgent({ ...overflowBaseRunParams, provider: "codex", model: "gpt-5.5", @@ -155,7 +155,7 @@ describe("runEmbeddedPiAgent Codex app-server recovery", () => { ); await expect( - runEmbeddedPiAgent({ + runEmbeddedAgent({ ...overflowBaseRunParams, provider: "codex", model: "gpt-5.5", @@ -182,7 +182,7 @@ describe("runEmbeddedPiAgent Codex app-server recovery", () => { }), ); - const promise = runEmbeddedPiAgent({ + const promise = runEmbeddedAgent({ ...overflowBaseRunParams, provider: "codex", model: "gpt-5.5", @@ -223,7 +223,7 @@ describe("runEmbeddedPiAgent Codex app-server recovery", () => { ); await expect( - runEmbeddedPiAgent({ + runEmbeddedAgent({ ...overflowBaseRunParams, provider: "codex", model: "gpt-5.5", diff --git a/src/agents/pi-embedded-runner/run.codex-server-error-fallback.test.ts b/src/agents/embedded-agent-runner/run.codex-server-error-fallback.test.ts similarity index 90% rename from src/agents/pi-embedded-runner/run.codex-server-error-fallback.test.ts rename to src/agents/embedded-agent-runner/run.codex-server-error-fallback.test.ts index da87b30bb24..c02a17996bc 100644 --- a/src/agents/pi-embedded-runner/run.codex-server-error-fallback.test.ts +++ b/src/agents/embedded-agent-runner/run.codex-server-error-fallback.test.ts @@ -14,11 +14,11 @@ import { resetRunOverflowCompactionHarnessMocks, } from "./run.overflow-compaction.harness.js"; -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; -describe("runEmbeddedPiAgent Codex server_error fallback handoff", () => { +describe("runEmbeddedAgent Codex server_error fallback handoff", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); }); beforeEach(() => { @@ -49,7 +49,7 @@ describe("runEmbeddedPiAgent Codex server_error fallback handoff", () => { }), ); - const promise = runEmbeddedPiAgent({ + const promise = runEmbeddedAgent({ ...overflowBaseRunParams, runId: "run-codex-server-error-fallback", config: makeModelFallbackCfg({ diff --git a/src/agents/pi-embedded-runner/run.compaction-loop-guard.test.ts b/src/agents/embedded-agent-runner/run.compaction-loop-guard.test.ts similarity index 95% rename from src/agents/pi-embedded-runner/run.compaction-loop-guard.test.ts rename to src/agents/embedded-agent-runner/run.compaction-loop-guard.test.ts index d23cee86f77..2f2bfc823ee 100644 --- a/src/agents/pi-embedded-runner/run.compaction-loop-guard.test.ts +++ b/src/agents/embedded-agent-runner/run.compaction-loop-guard.test.ts @@ -7,7 +7,7 @@ import type { import type { ToolOutcomeObserver, wrapToolWithBeforeToolCallHook as WrapToolWithBeforeToolCallHookType, -} from "../pi-tools.before-tool-call.js"; +} from "../agent-tools.before-tool-call.js"; import type { recordToolCall as RecordToolCallType, recordToolCallOutcome as RecordToolCallOutcomeType, @@ -32,7 +32,7 @@ import { resetRunOverflowCompactionHarnessMocks, } from "./run.overflow-compaction.harness.js"; -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; // These need to be imported AFTER loadRunOverflowCompactionHarness so that // they reference the same module instances the (re-imported) runner uses. // vi.resetModules() inside the harness invalidates any earlier import. @@ -96,15 +96,15 @@ async function executeWrappedToolOutcome( return tool.execute(`${toolName}-${liveToolCallSeq}`, toolParams, undefined, undefined); } -describe("post-compaction loop guard wired into runEmbeddedPiAgent", () => { +describe("post-compaction loop guard wired into runEmbeddedAgent", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); // Re-import after the harness reset so we share module instances with // the runner. The runner imports both modules through its own graph. ({ diagnosticSessionStates, getDiagnosticSessionState } = await import("../../logging/diagnostic-session-state.js")); ({ recordToolCall, recordToolCallOutcome } = await import("../tool-loop-detection.js")); - ({ wrapToolWithBeforeToolCallHook } = await import("../pi-tools.before-tool-call.js")); + ({ wrapToolWithBeforeToolCallHook } = await import("../agent-tools.before-tool-call.js")); ({ PostCompactionLoopPersistedError } = await import("./post-compaction-loop-guard.js")); }); @@ -178,7 +178,7 @@ describe("post-compaction loop guard wired into runEmbeddedPiAgent", () => { }), ); - await expect(runEmbeddedPiAgent(baseParams)).rejects.toBeInstanceOf( + await expect(runEmbeddedAgent(baseParams)).rejects.toBeInstanceOf( PostCompactionLoopPersistedError, ); @@ -223,7 +223,7 @@ describe("post-compaction loop guard wired into runEmbeddedPiAgent", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(result.meta.error).toBeUndefined(); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); @@ -259,7 +259,7 @@ describe("post-compaction loop guard wired into runEmbeddedPiAgent", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...baseParams, config: { tools: { @@ -306,7 +306,7 @@ describe("post-compaction loop guard wired into runEmbeddedPiAgent", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...baseParams, agentId: "agent-a", config: { @@ -366,7 +366,7 @@ describe("post-compaction loop guard wired into runEmbeddedPiAgent", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...baseParams, config: { tools: { @@ -435,7 +435,7 @@ describe("post-compaction loop guard wired into runEmbeddedPiAgent", () => { }), ); - await expect(runEmbeddedPiAgent(baseParams)).rejects.toBeInstanceOf( + await expect(runEmbeddedAgent(baseParams)).rejects.toBeInstanceOf( PostCompactionLoopPersistedError, ); diff --git a/src/agents/pi-embedded-runner/run.cross-provider-fallback-error-context.test.ts b/src/agents/embedded-agent-runner/run.cross-provider-fallback-error-context.test.ts similarity index 97% rename from src/agents/pi-embedded-runner/run.cross-provider-fallback-error-context.test.ts rename to src/agents/embedded-agent-runner/run.cross-provider-fallback-error-context.test.ts index a2fefba8e8e..661c0f7ceee 100644 --- a/src/agents/pi-embedded-runner/run.cross-provider-fallback-error-context.test.ts +++ b/src/agents/embedded-agent-runner/run.cross-provider-fallback-error-context.test.ts @@ -15,7 +15,7 @@ import { } from "./run.overflow-compaction.harness.js"; import type { EmbeddedRunAttemptResult } from "./run/types.js"; -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; const DEEPSEEK_ERROR_MESSAGE = "429 deepseek rate limit"; type CurrentAttemptAssistantWithError = NonNullable< EmbeddedRunAttemptResult["currentAttemptAssistant"] @@ -91,9 +91,9 @@ async function expectDeepseekFallbackError( expectDeepseekAssistant(getLastFormattedAssistant()); } -describe("runEmbeddedPiAgent cross-provider fallback error handling", () => { +describe("runEmbeddedAgent cross-provider fallback error handling", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); }); beforeEach(() => { @@ -124,7 +124,7 @@ describe("runEmbeddedPiAgent cross-provider fallback error handling", () => { }), ); - const promise = runEmbeddedPiAgent({ + const promise = runEmbeddedAgent({ ...overflowBaseRunParams, runId: "run-cross-provider-fallback-error-context", config: makeCrossProviderFallbackConfig(), @@ -158,7 +158,7 @@ describe("runEmbeddedPiAgent cross-provider fallback error handling", () => { }), ); - const promise = runEmbeddedPiAgent({ + const promise = runEmbeddedAgent({ ...overflowBaseRunParams, runId: "run-compaction-fallback-error-context", config: makeCrossProviderFallbackConfig(), diff --git a/src/agents/pi-embedded-runner/run.empty-error-retry.test.ts b/src/agents/embedded-agent-runner/run.empty-error-retry.test.ts similarity index 93% rename from src/agents/pi-embedded-runner/run.empty-error-retry.test.ts rename to src/agents/embedded-agent-runner/run.empty-error-retry.test.ts index fd8828bfc60..27c127e196e 100644 --- a/src/agents/pi-embedded-runner/run.empty-error-retry.test.ts +++ b/src/agents/embedded-agent-runner/run.empty-error-retry.test.ts @@ -10,7 +10,7 @@ import { } from "./run.overflow-compaction.harness.js"; import type { EmbeddedRunAttemptResult } from "./run/types.js"; -// Regression coverage for the silent-error retry in runEmbeddedPiAgent. +// Regression coverage for the silent-error retry in runEmbeddedAgent. // // Symptom: ollama/glm-5.1 occasionally ends a turn with stopReason="error" and // zero output tokens after a successful tool-call sequence. The user sees no @@ -18,7 +18,7 @@ import type { EmbeddedRunAttemptResult } from "./run/types.js"; // resubmission for errored turns, separate from the visible-answer retry used // for stopReason="stop" empty zero-token turns. -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; function emptyErrorAttempt( provider: string, @@ -50,9 +50,9 @@ function successAttempt(provider: string, model: string): EmbeddedRunAttemptResu }); } -describe("runEmbeddedPiAgent silent-error retry", () => { +describe("runEmbeddedAgent silent-error retry", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); }); beforeEach(() => { @@ -65,7 +65,7 @@ describe("runEmbeddedPiAgent silent-error retry", () => { mockedRunEmbeddedAttempt.mockResolvedValueOnce(emptyErrorAttempt("ollama", "glm-5.1:cloud")); mockedRunEmbeddedAttempt.mockResolvedValueOnce(successAttempt("ollama", "glm-5.1:cloud")); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "ollama", model: "glm-5.1:cloud", @@ -82,7 +82,7 @@ describe("runEmbeddedPiAgent silent-error retry", () => { mockedRunEmbeddedAttempt.mockResolvedValueOnce(emptyErrorAttempt("ollama", "glm-5.1:cloud")); } - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "ollama", model: "glm-5.1:cloud", @@ -100,7 +100,7 @@ describe("runEmbeddedPiAgent silent-error retry", () => { emptyErrorAttempt("ollama", "glm-5.1:cloud", 12), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "ollama", model: "glm-5.1:cloud", @@ -127,7 +127,7 @@ describe("runEmbeddedPiAgent silent-error retry", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "plain-provider", model: "plain-model", @@ -143,7 +143,7 @@ describe("runEmbeddedPiAgent silent-error retry", () => { ); mockedRunEmbeddedAttempt.mockResolvedValueOnce(successAttempt("anthropic", "claude-opus-4-7")); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "anthropic", model: "claude-opus-4-7", @@ -177,7 +177,7 @@ describe("runEmbeddedPiAgent silent-error retry", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "ollama", model: "glm-5.1:cloud", diff --git a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts b/src/agents/embedded-agent-runner/run.incomplete-turn.test.ts similarity index 98% rename from src/agents/pi-embedded-runner/run.incomplete-turn.test.ts rename to src/agents/embedded-agent-runner/run.incomplete-turn.test.ts index 6350bf52222..b37c867f68d 100644 --- a/src/agents/pi-embedded-runner/run.incomplete-turn.test.ts +++ b/src/agents/embedded-agent-runner/run.incomplete-turn.test.ts @@ -40,11 +40,11 @@ import { } from "./run/incomplete-turn.js"; import type { EmbeddedRunAttemptResult } from "./run/types.js"; -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; -describe("runEmbeddedPiAgent incomplete-turn safety", () => { +describe("runEmbeddedAgent incomplete-turn safety", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); }); beforeEach(() => { @@ -81,7 +81,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, runId: "run-before-agent-run-hook-block", }); @@ -115,7 +115,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-4.1", @@ -219,7 +219,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, trigger: "cron", provider: "openai", @@ -241,7 +241,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, prompt: "Please inspect the code, make the change, and run the checks.", sessionKey: undefined, @@ -252,7 +252,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { config: { agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "default", }, }, @@ -260,7 +260,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { { id: "main" }, { id: "research", - embeddedPi: { + embeddedAgent: { executionContract: "strict-agentic", }, }, @@ -290,7 +290,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, prompt: "Please inspect the code, make the change, and run the checks.", provider: "openai", @@ -299,7 +299,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { config: { agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "strict-agentic", }, }, @@ -336,7 +336,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai-codex", model: "gpt-5.5", @@ -370,7 +370,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, prompt: "Please inspect the code, make the change, and run the checks.", provider: "openai", @@ -414,7 +414,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, prompt: "Please inspect the code, make the change, and run the checks.", provider: "openai", @@ -423,7 +423,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { config: { agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "default", }, }, @@ -489,7 +489,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.4", @@ -523,7 +523,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, allowEmptyAssistantReplyAsSilent: true, provider: "openai-codex", @@ -564,7 +564,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.4", @@ -597,7 +597,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.4", @@ -633,7 +633,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "anthropic", model: "sonnet-4.6", @@ -693,7 +693,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "kimi", model: "kimi-for-coding", @@ -733,7 +733,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.4", @@ -787,7 +787,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "anthropic", model: "claude-opus-4.7", @@ -856,7 +856,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "llamacpp", model: "qwen3.6-27b", @@ -925,7 +925,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "sub2api", model: "claude-opus-4-7", @@ -953,7 +953,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.4", @@ -990,7 +990,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.4", @@ -1062,7 +1062,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { assistantTexts: ["I'll inspect the code, make the change, and run the checks."], toolMetas: [ { toolName: "read", meta: "path=src/index.ts" }, - { toolName: "search", meta: "pattern=runEmbeddedPiAgent" }, + { toolName: "search", meta: "pattern=runEmbeddedAgent" }, ], }), }); @@ -1383,7 +1383,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { expect(incompleteTurnText).toBeNull(); }); - it("surfaces an error for tool-use terminal turn with pre-tool text via runEmbeddedPiAgent (#76477)", async () => { + it("surfaces an error for tool-use terminal turn with pre-tool text via runEmbeddedAgent (#76477)", async () => { mockedClassifyFailoverReason.mockReturnValue(null); mockedRunEmbeddedAttempt.mockResolvedValueOnce( makeAttemptResult({ @@ -1401,7 +1401,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "anthropic", model: "sonnet-4.6", @@ -1899,7 +1899,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.4", @@ -2419,7 +2419,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, allowEmptyAssistantReplyAsSilent: true, provider: "openai-codex", @@ -2451,7 +2451,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.4", @@ -2535,7 +2535,7 @@ describe("runEmbeddedPiAgent incomplete-turn safety", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, prompt: "made a bunch of improvements to the student's source code (openclaw) this weekend, along with a few other maintainers. hopefully he will be more proactive now", diff --git a/src/agents/pi-embedded-runner/run.overflow-compaction.fixture.ts b/src/agents/embedded-agent-runner/run.overflow-compaction.fixture.ts similarity index 100% rename from src/agents/pi-embedded-runner/run.overflow-compaction.fixture.ts rename to src/agents/embedded-agent-runner/run.overflow-compaction.fixture.ts diff --git a/src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts b/src/agents/embedded-agent-runner/run.overflow-compaction.harness.ts similarity index 98% rename from src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts rename to src/agents/embedded-agent-runner/run.overflow-compaction.harness.ts index e503ef1a1a4..f41a691cfa4 100644 --- a/src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts +++ b/src/agents/embedded-agent-runner/run.overflow-compaction.harness.ts @@ -9,8 +9,8 @@ import type { PluginHookBeforePromptBuildResult, } from "../../plugins/types.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; +import type { FailoverReason } from "../embedded-agent-helpers/types.js"; import { clearAgentHarnesses, registerAgentHarness } from "../harness/registry.js"; -import type { FailoverReason } from "../pi-embedded-helpers/types.js"; import type { buildEmbeddedRunPayloads } from "./run/payloads.js"; import type { EmbeddedRunAttemptResult } from "./run/types.js"; @@ -426,7 +426,7 @@ export function resetRunOverflowCompactionHarnessMocks(): void { } export async function loadRunOverflowCompactionHarness(): Promise<{ - runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; + runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; }> { resetRunOverflowCompactionHarnessMocks(); vi.resetModules(); @@ -496,7 +496,7 @@ export async function loadRunOverflowCompactionHarness(): Promise<{ redactRunIdentifier: vi.fn((value?: string) => value ?? ""), })); - vi.doMock("../pi-embedded-helpers.js", () => ({ + vi.doMock("../embedded-agent-helpers.js", () => ({ formatBillingErrorMessage: mockedFormatBillingErrorMessage, classifyFailoverReason: mockedClassifyFailoverReason, extractObservedOverflowTokenCount: mockedExtractObservedOverflowTokenCount, @@ -606,6 +606,6 @@ export async function loadRunOverflowCompactionHarness(): Promise<{ }), })); - const { runEmbeddedPiAgent } = await import("./run.js"); - return { runEmbeddedPiAgent }; + const { runEmbeddedAgent } = await import("./run.js"); + return { runEmbeddedAgent }; } diff --git a/src/agents/pi-embedded-runner/run.overflow-compaction.loop.test.ts b/src/agents/embedded-agent-runner/run.overflow-compaction.loop.test.ts similarity index 94% rename from src/agents/pi-embedded-runner/run.overflow-compaction.loop.test.ts rename to src/agents/embedded-agent-runner/run.overflow-compaction.loop.test.ts index dd20d165ff7..65f8054c988 100644 --- a/src/agents/pi-embedded-runner/run.overflow-compaction.loop.test.ts +++ b/src/agents/embedded-agent-runner/run.overflow-compaction.loop.test.ts @@ -21,7 +21,7 @@ import { } from "./run.overflow-compaction.harness.js"; import type { EmbeddedRunAttemptResult } from "./run/types.js"; -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; function requireRecord(value: unknown, label: string): Record { if (!value || typeof value !== "object" || Array.isArray(value)) { @@ -58,7 +58,7 @@ function expectRetryContinuesFromTranscript() { describe("overflow compaction in run loop", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); }); beforeEach(() => { @@ -90,7 +90,7 @@ describe("overflow compaction in run loop", () => { compactDirect: mockedCompactDirect, }); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); const compactArg = requireMockCallArg(mockedCompactDirect, 0); @@ -129,7 +129,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...baseParams, currentMessageId: "telegram-msg-51024", }); @@ -163,7 +163,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...baseParams, currentMessageId: "telegram-msg-51025", }); @@ -191,7 +191,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); @@ -210,7 +210,7 @@ describe("overflow compaction in run loop", () => { reason: "nothing to compact", }); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1); @@ -234,7 +234,7 @@ describe("overflow compaction in run loop", () => { truncatedCount: 1, }); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect( @@ -282,7 +282,7 @@ describe("overflow compaction in run loop", () => { truncatedCount: 2, }); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); const oversizedArgs = requireMockCallArg(mockedSessionLikelyHasOversizedToolResults, 0); @@ -310,7 +310,7 @@ describe("overflow compaction in run loop", () => { ) .mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).not.toHaveBeenCalled(); expect(mockedTruncateOversizedToolResultsInSession).not.toHaveBeenCalled(); @@ -334,7 +334,7 @@ describe("overflow compaction in run loop", () => { ) .mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).not.toHaveBeenCalled(); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); @@ -363,7 +363,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect(mockedTruncateOversizedToolResultsInSession).not.toHaveBeenCalled(); @@ -396,7 +396,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); @@ -428,7 +428,7 @@ describe("overflow compaction in run loop", () => { truncatedCount: 2, }); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect(requireMockCallArg(mockedTruncateOversizedToolResultsInSession, 0).sessionFile).toBe( @@ -472,7 +472,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); // Compaction attempted 3 times (max) expect(mockedCompactDirect).toHaveBeenCalledTimes(3); @@ -506,7 +506,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(2); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(3); @@ -522,7 +522,7 @@ describe("overflow compaction in run loop", () => { makeAttemptResult({ promptError: compactionFailureError }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).not.toHaveBeenCalled(); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(1); @@ -550,7 +550,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); @@ -569,7 +569,7 @@ describe("overflow compaction in run loop", () => { }), ); - await expect(runEmbeddedPiAgent(baseParams)).rejects.toThrow("transport disconnected"); + await expect(runEmbeddedAgent(baseParams)).rejects.toThrow("transport disconnected"); expect(mockedCompactDirect).not.toHaveBeenCalled(); expectLogExcludes(mockedLog.warn, "source=assistantError"); @@ -585,7 +585,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(result.payloads?.[0]?.isError).toBe(true); expect(result.payloads?.[0]?.text).toContain("timed out"); @@ -608,7 +608,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(result.payloads).toEqual([ { @@ -640,7 +640,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(result.payloads).toBeUndefined(); expect(result.didSendViaMessagingTool).toBe(true); @@ -655,7 +655,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(result.payloads).toBeUndefined(); expect(result.didSendDeterministicApprovalPrompt).toBe(true); @@ -672,7 +672,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(result.payloads?.[0]?.isError).toBe(true); expect(result.payloads?.[0]?.text).toContain("timed out"); @@ -692,7 +692,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect( result.payloads?.map((payload) => ({ @@ -738,7 +738,7 @@ describe("overflow compaction in run loop", () => { }), ); - const result = await runEmbeddedPiAgent(baseParams); + const result = await runEmbeddedAgent(baseParams); expect(result.meta.agentMeta?.usage?.input).toBe(4_000); expect(result.meta.agentMeta?.promptTokens).toBe(2_000); diff --git a/src/agents/pi-embedded-runner/run.overflow-compaction.test.ts b/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts similarity index 97% rename from src/agents/pi-embedded-runner/run.overflow-compaction.test.ts rename to src/agents/embedded-agent-runner/run.overflow-compaction.test.ts index 49d362fb488..b6ebdea96a8 100644 --- a/src/agents/pi-embedded-runner/run.overflow-compaction.test.ts +++ b/src/agents/embedded-agent-runner/run.overflow-compaction.test.ts @@ -37,10 +37,10 @@ import { overflowBaseRunParams, resetRunOverflowCompactionHarnessMocks, } from "./run.overflow-compaction.harness.js"; -import type { RunEmbeddedPiAgentParams } from "./run/params.js"; +import type { RunEmbeddedAgentParams } from "./run/params.js"; import type { EmbeddedRunAttemptParams } from "./run/types.js"; -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; type RuntimePlanOverrides = Partial> & { auth?: Partial; resolvedRef?: Partial; @@ -67,7 +67,7 @@ function makeForwardingCase(internalEvents: AgentInternalEvent[]) { }, } satisfies { runId: string; - params: Partial; + params: Partial; expected: Record; }; } @@ -131,7 +131,7 @@ function makeForwardedRuntimePlan(overrides: RuntimePlanOverrides = {}): AgentRu resolvedRef: { provider: "anthropic", modelId: "test-model", - harnessId: "pi", + harnessId: "openclaw", }, tools: { normalize: vi.fn((tools) => tools), @@ -212,9 +212,9 @@ function expectRuntimePlanFields( } } -describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { +describe("runEmbeddedAgent overflow compaction trigger routing", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); }); beforeEach(() => { @@ -233,7 +233,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { mockedGlobalHookRunner.runBeforeAgentStart.mockResolvedValueOnce(legacyResult); mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "test-session", sessionKey: "test-key", sessionFile: "/tmp/session.json", @@ -252,7 +252,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { it("passes resolved auth profile into run attempts for context-engine afterTurn propagation", async () => { mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, runId: "run-auth-profile-passthrough", }); @@ -265,7 +265,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { it("uses the lightweight auth profile store during reply startup", async () => { mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, runId: "run-lightweight-auth-store", }); @@ -644,7 +644,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { mockedGetApiKeyForModel.mockRejectedValueOnce(new Error("generic auth should be skipped")); try { - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "anthropic", model: "test-model", @@ -681,7 +681,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { mockedBuildAgentRuntimePlan.mockReturnValueOnce(runtimePlan); mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, ...forwardingCase.params, runId: forwardingCase.runId, @@ -713,7 +713,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); const observedPriorities: unknown[] = []; - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, trigger: "user", runId: "run-user-session-priority", @@ -730,7 +730,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); const observedPriorities: unknown[] = []; - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, trigger: "cron", runId: "run-cron-session-priority", @@ -802,7 +802,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { mockedEnsureAuthProfileStoreWithoutExternalProfiles.mockReturnValueOnce(codexAuthStore); try { - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "codex", model: "gpt-5.4", @@ -883,7 +883,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { mockedGetApiKeyForModel.mockRejectedValueOnce(new Error("generic auth should be skipped")); try { - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.4", @@ -1000,7 +1000,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { mockedBuildAgentRuntimePlan.mockReturnValueOnce(runtimePlan); try { - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.5", @@ -1140,7 +1140,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { ); try { - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.5", @@ -1323,7 +1323,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { })); try { - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.5", @@ -1396,7 +1396,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { mockedGetApiKeyForModel.mockRejectedValueOnce(new Error("generic auth should be skipped")); try { - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.5", @@ -1468,7 +1468,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { mockedResolveAuthProfileOrder.mockReturnValueOnce(["openai-codex:default"]); try { - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.5", @@ -1553,7 +1553,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { }); try { - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.5", @@ -1687,7 +1687,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { })); try { - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, provider: "openai", model: "gpt-5.5", @@ -1755,7 +1755,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { }); await expect( - runEmbeddedPiAgent({ + runEmbeddedAgent({ ...overflowBaseRunParams, runId: "run-small-context", }), @@ -1772,7 +1772,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { compactDirect: mockedCompactDirect, }); - await runEmbeddedPiAgent(overflowBaseRunParams); + await runEmbeddedAgent(overflowBaseRunParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); const compactParams = expectMockCallFields(mockedCompactDirect, { @@ -1814,7 +1814,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { }), ); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); const compactParams = expectMockCallFields(mockedCompactDirect, {}); const runtimeContext = expectRecordFields(compactParams.runtimeContext, { @@ -1851,7 +1851,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { }), ); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expectMockCallFields(mockedCompactDirect, { currentTokenCount: 277403, @@ -1960,7 +1960,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { truncatedCount: 1, }); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(3); expect(mockedTruncateOversizedToolResultsInSession).toHaveBeenCalledTimes(1); @@ -1985,7 +1985,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { }, }); - await runEmbeddedPiAgent(overflowBaseRunParams); + await runEmbeddedAgent(overflowBaseRunParams); expectRecordFields(mockCallArg(mockedGlobalHookRunner.runBeforeCompaction), { messageCount: -1, @@ -2019,7 +2019,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { }, }); - await runEmbeddedPiAgent(overflowBaseRunParams); + await runEmbeddedAgent(overflowBaseRunParams); const maintenanceParams = expectMockCallFields(mockedRunContextEngineMaintenance, { contextEngine: mockedContextEngine, @@ -2053,7 +2053,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { }), ); - await runEmbeddedPiAgent(overflowBaseRunParams); + await runEmbeddedAgent(overflowBaseRunParams); expectMockCallFields( mockedRunEmbeddedAttempt, @@ -2079,7 +2079,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { ); mockedCompactDirect.mockRejectedValueOnce(new Error("engine boom")); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect(mockedGlobalHookRunner.runBeforeCompaction).toHaveBeenCalledTimes(1); @@ -2098,7 +2098,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { makeCompactionSuccess({ summary: "engine-owned compaction", tokensAfter: 50 }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, abortSignal: abortController.signal, }); @@ -2120,7 +2120,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { ); mockedPickFallbackThinkingLevel.mockReturnValue("low"); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(32); expect(mockedCompactDirect).not.toHaveBeenCalled(); @@ -2144,7 +2144,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { }), ); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expect(result.meta.error?.kind).toBe("retry_limit"); expect(result.meta.replayInvalid).toBe(true); @@ -2179,7 +2179,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => { mockedResolveFailoverStatus.mockReturnValue(429); await expect( - runEmbeddedPiAgent({ + runEmbeddedAgent({ ...overflowBaseRunParams, config: { agents: { diff --git a/src/agents/pi-embedded-runner/run.timeout-triggered-compaction.test.ts b/src/agents/embedded-agent-runner/run.timeout-triggered-compaction.test.ts similarity index 94% rename from src/agents/pi-embedded-runner/run.timeout-triggered-compaction.test.ts rename to src/agents/embedded-agent-runner/run.timeout-triggered-compaction.test.ts index 27aad7f6bda..b604fd14e5b 100644 --- a/src/agents/pi-embedded-runner/run.timeout-triggered-compaction.test.ts +++ b/src/agents/embedded-agent-runner/run.timeout-triggered-compaction.test.ts @@ -14,7 +14,7 @@ import { resetRunOverflowCompactionHarnessMocks, } from "./run.overflow-compaction.harness.js"; -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; const useTwoAuthProfiles = () => { mockedResolveAuthProfileOrder.mockReturnValue(["profile-a", "profile-b"]); @@ -108,7 +108,7 @@ function hookCallAt(index: number, kind: "before" | "after"): [HookEvent, HookCo describe("timeout-triggered compaction", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); }); beforeEach(() => { @@ -149,7 +149,7 @@ describe("timeout-triggered compaction", () => { // Retry after compaction succeeds mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); const compactParams = compactCallAt(0); @@ -201,7 +201,7 @@ describe("timeout-triggered compaction", () => { }), ); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); // Verify the loop continued (retry happened) expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); @@ -230,7 +230,7 @@ describe("timeout-triggered compaction", () => { ); mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ ...overflowBaseRunParams, messageChannel: "slack", messageProvider: "slack", @@ -269,7 +269,7 @@ describe("timeout-triggered compaction", () => { reason: "nothing to compact", }); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); // Compaction was attempted but failed → falls through to timeout error payload expect(mockedCompactDirect).toHaveBeenCalledTimes(1); @@ -289,7 +289,7 @@ describe("timeout-triggered compaction", () => { }), ); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); // No compaction attempt for low usage expect(mockedCompactDirect).not.toHaveBeenCalled(); @@ -308,7 +308,7 @@ describe("timeout-triggered compaction", () => { }), ); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expect(mockedCompactDirect).not.toHaveBeenCalled(); expect(result.payloads?.[0]?.isError).toBe(true); @@ -331,7 +331,7 @@ describe("timeout-triggered compaction", () => { ) .mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expect(mockedCompactDirect).not.toHaveBeenCalled(); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); @@ -355,7 +355,7 @@ describe("timeout-triggered compaction", () => { }), ); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); expect(mockedCompactDirect).not.toHaveBeenCalled(); @@ -382,7 +382,7 @@ describe("timeout-triggered compaction", () => { ); mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(1); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); @@ -400,7 +400,7 @@ describe("timeout-triggered compaction", () => { }), ); - await runEmbeddedPiAgent(overflowBaseRunParams); + await runEmbeddedAgent(overflowBaseRunParams); // timedOutDuringCompaction skips timeout-triggered compaction expect(mockedCompactDirect).not.toHaveBeenCalled(); @@ -451,7 +451,7 @@ describe("timeout-triggered compaction", () => { }), ); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); // Both compaction attempts used; third timeout falls through. expect(mockedCompactDirect).toHaveBeenCalledTimes(2); @@ -474,7 +474,7 @@ describe("timeout-triggered compaction", () => { // Compaction throws mockedCompactDirect.mockRejectedValueOnce(new Error("engine crashed")); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); // Should not crash — falls through to normal timeout handling expect(mockedCompactDirect).toHaveBeenCalledTimes(1); @@ -506,7 +506,7 @@ describe("timeout-triggered compaction", () => { }, }); - await runEmbeddedPiAgent(overflowBaseRunParams); + await runEmbeddedAgent(overflowBaseRunParams); const [beforeEvent, beforeContext] = hookCallAt(0, "before"); expect(beforeEvent).toEqual({ messageCount: -1, sessionFile: "/tmp/session.json" }); @@ -557,7 +557,7 @@ describe("timeout-triggered compaction", () => { reason: "nothing to compact", }); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(2); const firstCompact = compactCallAt(0); @@ -600,7 +600,7 @@ describe("timeout-triggered compaction", () => { .mockRejectedValueOnce(new Error("engine crashed")) .mockRejectedValueOnce(new Error("engine crashed again")); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); expect(mockedCompactDirect).toHaveBeenCalledTimes(2); expect(mockedRunEmbeddedAttempt).toHaveBeenCalledTimes(2); @@ -623,7 +623,7 @@ describe("timeout-triggered compaction", () => { }), ); - const result = await runEmbeddedPiAgent(overflowBaseRunParams); + const result = await runEmbeddedAgent(overflowBaseRunParams); // Despite high total tokens, low prompt tokens mean no compaction expect(mockedCompactDirect).not.toHaveBeenCalled(); diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/embedded-agent-runner/run.ts similarity index 98% rename from src/agents/pi-embedded-runner/run.ts rename to src/agents/embedded-agent-runner/run.ts index 165ad5ebe5d..2ee98248f9b 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/embedded-agent-runner/run.ts @@ -22,12 +22,17 @@ import { createAgentHarnessTaskRuntimeScope } from "../../tasks/agent-harness-ta import { sanitizeForLog } from "../../terminal/ansi.js"; import { resolveUserPath } from "../../utils.js"; import { isMarkdownCapableMessageChannel } from "../../utils/message-channel.js"; +import { + retireSessionMcpRuntime, + retireSessionMcpRuntimeForSessionKey, +} from "../agent-bundle-mcp-tools.js"; import { resolveAgentExecutionContract, resolveAgentDir, resolveSessionAgentIds, resolveAgentWorkspaceDir, } from "../agent-scope.js"; +import { resolveProcessToolScopeKey } from "../agent-tools.js"; import { type AuthProfileFailureReason, type AuthProfileStore, @@ -43,6 +48,22 @@ import { resolveStoredSessionKeyForSessionId, } from "../command/session.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js"; +import { + classifyFailoverReason, + extractObservedOverflowTokenCount, + type FailoverReason, + formatAssistantErrorText, + isAuthAssistantError, + isBillingAssistantError, + isCompactionFailureError, + isFailoverAssistantError, + isFailoverErrorMessage, + isLikelyContextOverflowError, + isRateLimitAssistantError, + parseImageDimensionError, + parseImageSizeError, + pickFallbackThinkingLevel, +} from "../embedded-agent-helpers.js"; import { isStrictAgenticExecutionContractActive } from "../execution-contract.js"; import { coerceToFailoverError, @@ -68,29 +89,8 @@ import { OPENAI_CODEX_PROVIDER_ID, listOpenAIAuthProfileProvidersForAgentRuntime, resolveContextConfigProviderForRuntime, - resolveSelectedOpenAIPiRuntimeProvider, + resolveSelectedOpenAIRuntimeProvider, } from "../openai-codex-routing.js"; -import { - retireSessionMcpRuntime, - retireSessionMcpRuntimeForSessionKey, -} from "../pi-bundle-mcp-tools.js"; -import { - classifyFailoverReason, - extractObservedOverflowTokenCount, - type FailoverReason, - formatAssistantErrorText, - isAuthAssistantError, - isBillingAssistantError, - isCompactionFailureError, - isFailoverAssistantError, - isFailoverErrorMessage, - isLikelyContextOverflowError, - isRateLimitAssistantError, - parseImageDimensionError, - parseImageSizeError, - pickFallbackThinkingLevel, -} from "../pi-embedded-helpers.js"; -import { resolveProcessToolScopeKey } from "../pi-tools.js"; import { resolveProviderIdForAuth } from "../provider-auth-aliases.js"; import { runAgentCleanupStep } from "../run-cleanup-timeout.js"; import { buildAgentRuntimeAuthPlan } from "../runtime-plan/auth.js"; @@ -175,7 +175,7 @@ import { resolveRunLivenessState, shouldTreatEmptyAssistantReplyAsSilent, } from "./run/incomplete-turn.js"; -import type { RunEmbeddedPiAgentParams } from "./run/params.js"; +import type { RunEmbeddedAgentParams } from "./run/params.js"; import { buildEmbeddedRunPayloads } from "./run/payloads.js"; import { handleRetryLimitExhaustion } from "./run/retry-limit.js"; import { @@ -190,8 +190,8 @@ import { truncateOversizedToolResultsInSession, } from "./tool-result-truncation.js"; import type { - EmbeddedPiAgentMeta, - EmbeddedPiRunResult, + EmbeddedAgentMeta, + EmbeddedAgentRunResult, TraceAttempt, ToolSummaryTrace, EmbeddedRunLivenessState, @@ -236,7 +236,7 @@ function withEmbeddedRunLaneTimeout( } function resolveEmbeddedRunSessionQueuePriority( - trigger: RunEmbeddedPiAgentParams["trigger"], + trigger: RunEmbeddedAgentParams["trigger"], ): CommandQueueEnqueueOptions["priority"] { switch (trigger) { case "user": @@ -358,7 +358,7 @@ function buildTraceToolSummary(params: { * See: https://github.com/openclaw/openclaw/issues/60552 */ function backfillSessionKey(params: { - config: RunEmbeddedPiAgentParams["config"]; + config: RunEmbeddedAgentParams["config"]; sessionId: string; sessionKey?: string; agentId?: string; @@ -406,9 +406,9 @@ function buildHandledReplyPayloads(reply?: ReplyPayload) { ]; } -export async function runEmbeddedPiAgent( - params: RunEmbeddedPiAgentParams, -): Promise { +export async function runEmbeddedAgent( + params: RunEmbeddedAgentParams, +): Promise { // Resolve sessionKey early so all downstream consumers (hooks, LCM, compaction) // receive a non-null key even when callers omit it. See #60552. const effectiveSessionKey = backfillSessionKey({ @@ -487,9 +487,9 @@ export async function runEmbeddedPiAgent( const startupStages = createEmbeddedRunStageTracker(); let startupStagesEmitted = false; const notifyExecutionPhase = ( - phase: Parameters>[0]["phase"], + phase: Parameters>[0]["phase"], extra?: Omit< - Parameters>[0], + Parameters>[0], "phase" >, ) => { @@ -497,7 +497,7 @@ export async function runEmbeddedPiAgent( params.onExecutionPhase?.({ phase, ...extra }); }; const notifyRunProgress = ( - info: Parameters>[0], + info: Parameters>[0], ) => { noteLaneTaskProgress(); params.onRunProgress?.(info); @@ -536,7 +536,7 @@ export async function runEmbeddedPiAgent( const redactedWorkspace = redactRunIdentifier(resolvedWorkspace); if (workspaceResolution.usedFallback) { log.warn( - `[workspace-fallback] caller=runEmbeddedPiAgent reason=${workspaceResolution.fallbackReason} run=${params.runId} session=${redactedSessionId} sessionKey=${redactedSessionKey} agent=${workspaceResolution.agentId} workspace=${redactedWorkspace}`, + `[workspace-fallback] caller=runEmbeddedAgent reason=${workspaceResolution.fallbackReason} run=${params.runId} session=${redactedSessionId} sessionKey=${redactedSessionKey} agent=${workspaceResolution.agentId} workspace=${redactedWorkspace}`, ); } startupStages.mark("workspace"); @@ -628,9 +628,9 @@ export async function runEmbeddedPiAgent( agentHarnessId: params.agentHarnessId, agentHarnessRuntimeOverride: params.agentHarnessRuntimeOverride, }); - const pluginHarnessOwnsTransport = agentHarness.id !== "pi"; + const pluginHarnessOwnsTransport = agentHarness.id !== "openclaw"; const modelConfigProvider = provider; - const selectedPiRuntimeProvider = resolveSelectedOpenAIPiRuntimeProvider({ + const selectedRuntimeProvider = resolveSelectedOpenAIRuntimeProvider({ provider, harnessRuntime: agentHarness.id, agentHarnessId: agentHarness.id, @@ -646,9 +646,9 @@ export async function runEmbeddedPiAgent( params.config, { // Plugin dynamic model hooks can resolve explicit model refs without - // first generating PI models.json. This keeps one-shot model runs from + // first generating OpenClaw models.json. This keeps one-shot model runs from // blocking on unrelated provider discovery. - skipPiDiscovery: true, + skipAgentDiscovery: true, workspaceDir: resolvedWorkspace, }, ); @@ -663,19 +663,19 @@ export async function runEmbeddedPiAgent( workspaceDir: resolvedWorkspace, }); })(); - if (selectedPiRuntimeProvider !== provider && modelResolution.model) { + if (selectedRuntimeProvider !== provider && modelResolution.model) { const runtimeModelResolution = await resolveModelAsync( - selectedPiRuntimeProvider, + selectedRuntimeProvider, modelId, agentDir, params.config, { - skipPiDiscovery: true, + skipAgentDiscovery: true, workspaceDir: resolvedWorkspace, }, ); if (runtimeModelResolution.model) { - provider = selectedPiRuntimeProvider; + provider = selectedRuntimeProvider; modelResolution = runtimeModelResolution; } } @@ -1019,7 +1019,7 @@ export async function runEmbeddedPiAgent( ? advancePluginHarnessAuthProfile : advanceAuthProfile; - // Plugin harnesses own their model transport/auth. Running PI's generic + // Plugin harnesses own their model transport/auth. Running OpenClaw's generic // auth bootstrap here can turn synthetic provider markers into real // vendor-token refresh attempts before the plugin gets control. if (!pluginHarnessOwnsTransport || pluginHarnessNeedsOpenClawAuthBootstrap) { @@ -1141,10 +1141,10 @@ export async function runEmbeddedPiAgent( let activeSessionId = params.sessionId; let activeSessionFile = params.sessionFile; let suppressNextUserMessagePersistence = params.suppressNextUserMessagePersistence ?? false; - // Pi owns JSONL persistence; this marker only lets the outer retry avoid + // The embedded agent owns JSONL persistence; this marker lets the outer retry avoid // replaying the same inbound channel message after overflow compaction. let lastPersistedCurrentMessageId: string | number | undefined; - const onUserMessagePersisted: RunEmbeddedPiAgentParams["onUserMessagePersisted"] = ( + const onUserMessagePersisted: RunEmbeddedAgentParams["onUserMessagePersisted"] = ( message, ) => { if (params.currentMessageId !== undefined) { @@ -1187,8 +1187,8 @@ export async function runEmbeddedPiAgent( const maybeMarkAuthProfileFailure = async (failure: { profileId?: string; reason?: AuthProfileFailureReason | null; - config?: RunEmbeddedPiAgentParams["config"]; - agentDir?: RunEmbeddedPiAgentParams["agentDir"]; + config?: RunEmbeddedAgentParams["config"]; + agentDir?: RunEmbeddedAgentParams["agentDir"]; modelId?: string; }) => { const { profileId, reason } = failure; @@ -1282,7 +1282,7 @@ export async function runEmbeddedPiAgent( ...(params.sessionKey ? { sessionKey: params.sessionKey } : {}), }); }; - // When the engine owns compaction, compactEmbeddedPiSessionDirect is + // When the engine owns compaction, compactEmbeddedAgentSessionDirect is // bypassed. Fire lifecycle hooks here so recovery paths still notify // subscribers like memory extensions and usage trackers. const runOwnsCompactionBeforeHook = async (reason: string) => { @@ -1495,8 +1495,8 @@ export async function runEmbeddedPiAgent( provider, modelId, // Use the harness selected before model/auth setup for the actual - // attempt too. Otherwise plugin-owned transports can skip PI auth - // bootstrap but drift back to PI when the attempt is created. + // attempt too. Otherwise plugin-owned transports can skip OpenClaw auth + // bootstrap but drift back to OpenClaw when the attempt is created. agentHarnessId: agentHarness.id, ...(params.sessionKey ? { @@ -2130,7 +2130,7 @@ export async function runEmbeddedPiAgent( params.currentMessageId !== undefined && params.currentMessageId === lastPersistedCurrentMessageId ) { - // The first attempt reached Pi far enough to persist this user turn. + // The first attempt reached the embedded agent far enough to persist this user turn. // Retrying the original prompt would replay it, so resume from the // compacted transcript and suppress the next user append. nextAttemptPromptOverride = MID_TURN_PRECHECK_CONTINUATION_PROMPT; @@ -2750,7 +2750,7 @@ export async function runEmbeddedPiAgent( model: model.id, assistant: sessionLastAssistant, }); - const agentMeta: EmbeddedPiAgentMeta = { + const agentMeta: EmbeddedAgentMeta = { sessionId: sessionIdUsed, sessionFile: sessionFileUsed, provider: reportedModelRef.provider, diff --git a/src/agents/pi-embedded-runner/run/AGENTS.md b/src/agents/embedded-agent-runner/run/AGENTS.md similarity index 100% rename from src/agents/pi-embedded-runner/run/AGENTS.md rename to src/agents/embedded-agent-runner/run/AGENTS.md diff --git a/src/agents/pi-embedded-runner/run/CLAUDE.md b/src/agents/embedded-agent-runner/run/CLAUDE.md similarity index 100% rename from src/agents/pi-embedded-runner/run/CLAUDE.md rename to src/agents/embedded-agent-runner/run/CLAUDE.md diff --git a/src/agents/pi-embedded-runner/run/abortable.test.ts b/src/agents/embedded-agent-runner/run/abortable.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/abortable.test.ts rename to src/agents/embedded-agent-runner/run/abortable.test.ts diff --git a/src/agents/pi-embedded-runner/run/abortable.ts b/src/agents/embedded-agent-runner/run/abortable.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/abortable.ts rename to src/agents/embedded-agent-runner/run/abortable.ts diff --git a/src/agents/pi-embedded-runner/run/assistant-failover.test.ts b/src/agents/embedded-agent-runner/run/assistant-failover.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/assistant-failover.test.ts rename to src/agents/embedded-agent-runner/run/assistant-failover.test.ts index 45cc393cb12..f319b70a8aa 100644 --- a/src/agents/pi-embedded-runner/run/assistant-failover.test.ts +++ b/src/agents/embedded-agent-runner/run/assistant-failover.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; +import { formatBillingErrorMessage } from "../../embedded-agent-helpers.js"; import { FailoverError } from "../../failover-error.js"; -import { formatBillingErrorMessage } from "../../pi-embedded-helpers.js"; import { handleAssistantFailover } from "./assistant-failover.js"; type Params = Parameters[0]; diff --git a/src/agents/pi-embedded-runner/run/assistant-failover.ts b/src/agents/embedded-agent-runner/run/assistant-failover.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/assistant-failover.ts rename to src/agents/embedded-agent-runner/run/assistant-failover.ts index 10ab2300146..1f5fefada50 100644 --- a/src/agents/pi-embedded-runner/run/assistant-failover.ts +++ b/src/agents/embedded-agent-runner/run/assistant-failover.ts @@ -1,14 +1,14 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { sanitizeForLog } from "../../../terminal/ansi.js"; import type { AuthProfileFailureReason } from "../../auth-profiles.js"; -import { FailoverError, resolveFailoverStatus } from "../../failover-error.js"; import { formatAssistantErrorText, formatBillingErrorMessage, isTimeoutErrorMessage, type FailoverReason, -} from "../../pi-embedded-helpers.js"; +} from "../../embedded-agent-helpers.js"; +import { FailoverError, resolveFailoverStatus } from "../../failover-error.js"; import { mergeRetryFailoverReason, resolveRunFailoverDecision, diff --git a/src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts b/src/agents/embedded-agent-runner/run/attempt-bootstrap-routing.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt-bootstrap-routing.ts rename to src/agents/embedded-agent-runner/run/attempt-bootstrap-routing.ts diff --git a/src/agents/pi-embedded-runner/run/attempt-http-runtime.ts b/src/agents/embedded-agent-runner/run/attempt-http-runtime.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt-http-runtime.ts rename to src/agents/embedded-agent-runner/run/attempt-http-runtime.ts diff --git a/src/agents/pi-embedded-runner/run/attempt-session.ts b/src/agents/embedded-agent-runner/run/attempt-session.ts similarity index 89% rename from src/agents/pi-embedded-runner/run/attempt-session.ts rename to src/agents/embedded-agent-runner/run/attempt-session.ts index fe2bbdcbcab..31a6502bf5a 100644 --- a/src/agents/pi-embedded-runner/run/attempt-session.ts +++ b/src/agents/embedded-agent-runner/run/attempt-session.ts @@ -1,4 +1,4 @@ -import type { CreateAgentSessionOptions } from "@earendil-works/pi-coding-agent"; +import type { CreateAgentSessionOptions } from "../../sessions/index.js"; export type EmbeddedAgentSessionOptions = { cwd: string; diff --git a/src/agents/pi-embedded-runner/run/attempt-stage-timing.test.ts b/src/agents/embedded-agent-runner/run/attempt-stage-timing.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt-stage-timing.test.ts rename to src/agents/embedded-agent-runner/run/attempt-stage-timing.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt-stage-timing.ts b/src/agents/embedded-agent-runner/run/attempt-stage-timing.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt-stage-timing.ts rename to src/agents/embedded-agent-runner/run/attempt-stage-timing.ts diff --git a/src/agents/pi-embedded-runner/run/attempt-system-prompt.test.ts b/src/agents/embedded-agent-runner/run/attempt-system-prompt.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt-system-prompt.test.ts rename to src/agents/embedded-agent-runner/run/attempt-system-prompt.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt-system-prompt.ts b/src/agents/embedded-agent-runner/run/attempt-system-prompt.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt-system-prompt.ts rename to src/agents/embedded-agent-runner/run/attempt-system-prompt.ts diff --git a/src/agents/pi-embedded-runner/run/attempt-tool-construction-plan.test.ts b/src/agents/embedded-agent-runner/run/attempt-tool-construction-plan.test.ts similarity index 92% rename from src/agents/pi-embedded-runner/run/attempt-tool-construction-plan.test.ts rename to src/agents/embedded-agent-runner/run/attempt-tool-construction-plan.test.ts index 8c54161fe24..c69b643233c 100644 --- a/src/agents/pi-embedded-runner/run/attempt-tool-construction-plan.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt-tool-construction-plan.test.ts @@ -130,6 +130,38 @@ describe("applyEmbeddedAttemptToolsAllow", () => { ).toEqual(["memory_search", "memory_get"]); }); + it("filters bundled runtime tools by explicit tool name and bundled plugin id", () => { + const tools = [ + { name: "strict__strict_probe" }, + { name: "loose__extra_probe" }, + { name: "lsp_hover_typescript" }, + { name: "lsp_definition_typescript" }, + ]; + const toolMeta = (tool: { name: string }) => { + if (tool.name.includes("__")) { + return { pluginId: "bundle-mcp" }; + } + if (tool.name.startsWith("lsp_")) { + return { pluginId: "bundle-lsp" }; + } + return undefined; + }; + + expect( + applyEmbeddedAttemptToolsAllow(tools, ["strict__strict_probe"], { toolMeta }).map( + (tool) => tool.name, + ), + ).toEqual(["strict__strict_probe"]); + expect( + applyEmbeddedAttemptToolsAllow(tools, ["lsp_hover_typescript"], { toolMeta }).map( + (tool) => tool.name, + ), + ).toEqual(["lsp_hover_typescript"]); + expect( + applyEmbeddedAttemptToolsAllow(tools, ["bundle-mcp"], { toolMeta }).map((tool) => tool.name), + ).toEqual(["strict__strict_probe", "loose__extra_probe"]); + }); + it("treats an explicit empty toolsAllow as no tools", () => { const tools = [{ name: "exec" }, { name: "read" }, { name: "message" }]; diff --git a/src/agents/pi-embedded-runner/run/attempt-tool-construction-plan.ts b/src/agents/embedded-agent-runner/run/attempt-tool-construction-plan.ts similarity index 98% rename from src/agents/pi-embedded-runner/run/attempt-tool-construction-plan.ts rename to src/agents/embedded-agent-runner/run/attempt-tool-construction-plan.ts index d9d6827879a..262efa53bac 100644 --- a/src/agents/pi-embedded-runner/run/attempt-tool-construction-plan.ts +++ b/src/agents/embedded-agent-runner/run/attempt-tool-construction-plan.ts @@ -1,5 +1,5 @@ -import { TOOL_NAME_SEPARATOR } from "../../pi-bundle-mcp-names.js"; -import type { OpenClawCodingToolConstructionPlan } from "../../pi-tools.js"; +import { TOOL_NAME_SEPARATOR } from "../../agent-bundle-mcp-names.js"; +import type { OpenClawCodingToolConstructionPlan } from "../../agent-tools.js"; import { isToolAllowedByPolicyName } from "../../tool-policy-match.js"; import { buildPluginToolGroups, diff --git a/src/agents/pi-embedded-runner/run/attempt-trajectory-status.test.ts b/src/agents/embedded-agent-runner/run/attempt-trajectory-status.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt-trajectory-status.test.ts rename to src/agents/embedded-agent-runner/run/attempt-trajectory-status.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt-trajectory-status.ts b/src/agents/embedded-agent-runner/run/attempt-trajectory-status.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt-trajectory-status.ts rename to src/agents/embedded-agent-runner/run/attempt-trajectory-status.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts b/src/agents/embedded-agent-runner/run/attempt.context-engine-helpers.ts similarity index 97% rename from src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts rename to src/agents/embedded-agent-runner/run/attempt.context-engine-helpers.ts index e44ae706ab3..91f0cd68977 100644 --- a/src/agents/pi-embedded-runner/run/attempt.context-engine-helpers.ts +++ b/src/agents/embedded-agent-runner/run/attempt.context-engine-helpers.ts @@ -1,7 +1,7 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import type { ContextEngine } from "../../../context-engine/types.js"; import type { BootstrapMode } from "../../bootstrap-mode.js"; +import type { AgentMessage } from "../../runtime/index.js"; import { normalizeUsage, type NormalizedUsage } from "../../usage.js"; import type { PromptCacheChange } from "../prompt-cache-observability.js"; import type { EmbeddedRunAttemptResult } from "./types.js"; diff --git a/src/agents/pi-embedded-runner/run/attempt.memory-flush-forwarding.test.ts b/src/agents/embedded-agent-runner/run/attempt.memory-flush-forwarding.test.ts similarity index 93% rename from src/agents/pi-embedded-runner/run/attempt.memory-flush-forwarding.test.ts rename to src/agents/embedded-agent-runner/run/attempt.memory-flush-forwarding.test.ts index 23651367871..e94e54c6a5f 100644 --- a/src/agents/pi-embedded-runner/run/attempt.memory-flush-forwarding.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.memory-flush-forwarding.test.ts @@ -1,10 +1,10 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { Api, Model } from "@earendil-works/pi-ai"; -import type { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent"; +import type { AuthStorage, ModelRegistry } from "openclaw/plugin-sdk/agent-sessions"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; -import type { AnyAgentTool } from "../../pi-tools.types.js"; +import type { AnyAgentTool } from "../../agent-tools.types.js"; import { buildEmbeddedAttemptToolRunContext } from "./attempt.tool-run-context.js"; const MEMORY_RELATIVE_PATH = "memory/2026-03-24.md"; @@ -26,7 +26,7 @@ function createAttemptParams(workspaceDir: string) { id: "gpt-5.4", input: ["text"], contextWindow: 128_000, - } as Model, + } as Model, authStorage: {} as AuthStorage, modelRegistry: {} as ModelRegistry, thinkLevel: "off" as const, @@ -65,7 +65,7 @@ describe("runEmbeddedAttempt memory flush tool forwarding", () => { await fs.mkdir(path.dirname(memoryFile), { recursive: true }); await fs.writeFile(memoryFile, "seed", "utf-8"); - const { wrapToolMemoryFlushAppendOnlyWrite } = await import("../../pi-tools.read.js"); + const { wrapToolMemoryFlushAppendOnlyWrite } = await import("../../agent-tools.read.js"); const fallbackWrite = vi.fn(async () => { throw new Error("append-only wrapper should not delegate to the base write tool"); }); diff --git a/src/agents/pi-embedded-runner/run/attempt.model-diagnostic-events.test.ts b/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/attempt.model-diagnostic-events.test.ts rename to src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.test.ts index cf28872f2eb..f9fad48c174 100644 --- a/src/agents/pi-embedded-runner/run/attempt.model-diagnostic-events.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.test.ts @@ -1,4 +1,4 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { onInternalDiagnosticEvent, diff --git a/src/agents/pi-embedded-runner/run/attempt.model-diagnostic-events.ts b/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/attempt.model-diagnostic-events.ts rename to src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.ts index e621fa52c17..0c159cd5214 100644 --- a/src/agents/pi-embedded-runner/run/attempt.model-diagnostic-events.ts +++ b/src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.ts @@ -1,4 +1,3 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; import { fireAndForgetBoundedHook } from "../../../hooks/fire-and-forget.js"; import { diagnosticErrorCategory, @@ -28,6 +27,7 @@ import type { PluginHookModelCallEndedEvent, PluginHookModelCallStartedEvent, } from "../../../plugins/hook-types.js"; +import type { StreamFn } from "../../runtime/index.js"; export { diagnosticErrorCategory }; diff --git a/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts b/src/agents/embedded-agent-runner/run/attempt.prompt-helpers.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.prompt-helpers.test.ts rename to src/agents/embedded-agent-runner/run/attempt.prompt-helpers.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts b/src/agents/embedded-agent-runner/run/attempt.prompt-helpers.ts similarity index 98% rename from src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts rename to src/agents/embedded-agent-runner/run/attempt.prompt-helpers.ts index afe06536d56..fa964ca8fbe 100644 --- a/src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts +++ b/src/agents/embedded-agent-runner/run/attempt.prompt-helpers.ts @@ -14,12 +14,11 @@ import type { } from "../../../plugins/types.js"; import { isCronSessionKey, isSubagentSessionKey } from "../../../routing/session-key.js"; import { joinPresentTextSegments } from "../../../shared/text/join-segments.js"; +import { resolveProcessToolScopeKey } from "../../agent-tools.js"; import { listActiveProcessSessionReferences } from "../../bash-process-references.js"; import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js"; import { buildActiveImageGenerationTaskPromptContextForSession } from "../../image-generation-task-status.js"; import { buildActiveMusicGenerationTaskPromptContextForSession } from "../../music-generation-task-status.js"; -import { hasOpenAICompatibleConversationTurn } from "../../openai-compatible-conversation-turn.js"; -import { resolveProcessToolScopeKey } from "../../pi-tools.js"; import { prependSystemPromptAdditionAfterCacheBoundary } from "../../system-prompt-cache-boundary.js"; import { resolveEffectiveToolFsWorkspaceOnly } from "../../tool-fs-policy.js"; import { derivePromptTokens, type NormalizedUsage } from "../../usage.js"; @@ -252,9 +251,7 @@ export function resolvePromptSubmissionSkipReason(params: { if (params.prompt.trim().length > 0 || params.imageCount > 0) { return null; } - return hasOpenAICompatibleConversationTurn(params.messages) - ? "blank_user_prompt" - : "empty_prompt_history_images"; + return params.messages.length > 0 ? "blank_user_prompt" : "empty_prompt_history_images"; } const QUEUED_USER_MESSAGE_MARKER = diff --git a/src/agents/pi-embedded-runner/run/attempt.queue-message.test.ts b/src/agents/embedded-agent-runner/run/attempt.queue-message.test.ts similarity index 91% rename from src/agents/pi-embedded-runner/run/attempt.queue-message.test.ts rename to src/agents/embedded-agent-runner/run/attempt.queue-message.test.ts index 27d3303e4c7..1d99d970fea 100644 --- a/src/agents/pi-embedded-runner/run/attempt.queue-message.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.queue-message.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from "vitest"; -import { testing, type EmbeddedPiActiveSessionSteerTarget } from "./attempt.js"; +import { testing, type EmbeddedAgentActiveSessionSteerTarget } from "./attempt.js"; -describe("embedded Pi queued steering cancellation", () => { +describe("embedded OpenClaw queued steering cancellation", () => { it("waits for the queued user message_end transcript boundary", async () => { let emit!: (event: unknown) => void; - const activeSession: EmbeddedPiActiveSessionSteerTarget = { + const activeSession: EmbeddedAgentActiveSessionSteerTarget = { getSteeringMessages: () => [], steer: async () => {}, subscribe: (listener) => { @@ -67,7 +67,7 @@ describe("embedded Pi queued steering cancellation", () => { }; const steeringUiMessages = ["keep this rich payload", "timed-out completion announce"]; const queueMessages = [unrelatedMessage, targetMessage, trailingMessage]; - const activeSession: EmbeddedPiActiveSessionSteerTarget = { + const activeSession: EmbeddedAgentActiveSessionSteerTarget = { agent: { steeringQueue: { messages: queueMessages, @@ -105,7 +105,7 @@ describe("embedded Pi queued steering cancellation", () => { const steeringUiMessages = ["completion after parent stopped", "keep unrelated queue entry"]; const queueMessages = [targetMessage, keepMessage]; let unsubscribed = false; - const activeSession: EmbeddedPiActiveSessionSteerTarget = { + const activeSession: EmbeddedAgentActiveSessionSteerTarget = { agent: { steeringQueue: { messages: queueMessages, @@ -143,7 +143,7 @@ describe("embedded Pi queued steering cancellation", () => { } }); - it("keeps queued steering pending when Pi auto-retry starts after agent_end", async () => { + it("keeps queued steering pending when auto-retry starts after agent_end", async () => { vi.useFakeTimers(); try { let emit!: (event: unknown) => void; @@ -154,7 +154,7 @@ describe("embedded Pi queued steering cancellation", () => { }; const steeringUiMessages = ["completion survives retry"]; const queueMessages = [targetMessage]; - const activeSession: EmbeddedPiActiveSessionSteerTarget = { + const activeSession: EmbeddedAgentActiveSessionSteerTarget = { agent: { steeringQueue: { messages: queueMessages, @@ -195,7 +195,7 @@ describe("embedded Pi queued steering cancellation", () => { } }); - it("keeps queued steering pending when Pi auto-compaction starts after agent_end", async () => { + it("keeps queued steering pending when auto-compaction starts after agent_end", async () => { vi.useFakeTimers(); try { let emit!: (event: unknown) => void; @@ -206,7 +206,7 @@ describe("embedded Pi queued steering cancellation", () => { }; const steeringUiMessages = ["completion survives compaction"]; const queueMessages = [targetMessage]; - const activeSession: EmbeddedPiActiveSessionSteerTarget = { + const activeSession: EmbeddedAgentActiveSessionSteerTarget = { agent: { steeringQueue: { messages: queueMessages, diff --git a/src/agents/pi-embedded-runner/run/attempt.session-lock.test.ts b/src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts similarity index 98% rename from src/agents/pi-embedded-runner/run/attempt.session-lock.test.ts rename to src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts index 89db2c08aec..8424d3ddd6b 100644 --- a/src/agents/pi-embedded-runner/run/attempt.session-lock.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.session-lock.test.ts @@ -284,7 +284,7 @@ describe("embedded attempt session lock lifecycle", () => { ]); }); - it("drains queued Pi session events before reacquiring for cleanup", async () => { + it("drains queued OpenClaw session events before reacquiring for cleanup", async () => { const events: string[] = []; let resolveQueue!: () => void; const session = { @@ -1101,7 +1101,7 @@ describe("embedded attempt session lock lifecycle", () => { const hasHandlers = vi.fn(() => false); const session = { _extensionRunner: { hasHandlers }, - _processAgentEvent: vi.fn(async (event: { type?: string }) => { + _handleAgentEvent: vi.fn(async (event: { type?: string }) => { processed.push(event.type); }), }; @@ -1118,11 +1118,11 @@ describe("embedded attempt session lock lifecycle", () => { }, }); - await session["_processAgentEvent"]({ type: "message_update" }); - await session["_processAgentEvent"]({ type: "tool_execution_end" }); - await session["_processAgentEvent"]({ type: "message_end" }); - await session["_processAgentEvent"]({ type: "agent_end" }); - await session["_processAgentEvent"]({}); + await session["_handleAgentEvent"]({ type: "message_update" }); + await session["_handleAgentEvent"]({ type: "tool_execution_end" }); + await session["_handleAgentEvent"]({ type: "message_end" }); + await session["_handleAgentEvent"]({ type: "agent_end" }); + await session["_handleAgentEvent"]({}); expect(processed).toEqual([ "message_update", @@ -1137,7 +1137,7 @@ describe("embedded attempt session lock lifecycle", () => { expect(releases).toEqual(["released", "released", "released"]); }); - it("makes the Pi event listener await locked session event processing", async () => { + it("makes the OpenClaw event listener await locked session event processing", async () => { const events: string[] = []; const session = { _agentEventQueue: Promise.resolve(), @@ -1167,20 +1167,29 @@ describe("embedded attempt session lock lifecycle", () => { const result = handleAgentEvent({ type: "message_end" }) as unknown as Promise; expect(result).toHaveProperty("then"); - expect(events).toEqual(["disconnect", "reconnect", "handle:message_end"]); + expect(events).toEqual([ + "disconnect", + "reconnect", + "disconnect", + "reconnect", + "lock", + "handle:message_end", + ]); await result; expect(events).toEqual([ "disconnect", "reconnect", - "handle:message_end", + "disconnect", + "reconnect", "lock", + "handle:message_end", "process:message_end", ]); }); - it("locks Pi extension hooks that can mutate the session outside agent events", async () => { + it("locks OpenClaw extension hooks that can mutate the session outside agent events", async () => { const locked: string[] = []; const called: string[] = []; const hasHandlers = vi.fn( diff --git a/src/agents/pi-embedded-runner/run/attempt.session-lock.ts b/src/agents/embedded-agent-runner/run/attempt.session-lock.ts similarity index 97% rename from src/agents/pi-embedded-runner/run/attempt.session-lock.ts rename to src/agents/embedded-agent-runner/run/attempt.session-lock.ts index 7aceae13f1a..c088d7fa38f 100644 --- a/src/agents/pi-embedded-runner/run/attempt.session-lock.ts +++ b/src/agents/embedded-agent-runner/run/attempt.session-lock.ts @@ -27,7 +27,9 @@ type SessionWriteLockRunOptions = { }; type SessionEventProcessor = { - _processAgentEvent?: (event: unknown) => Promise; + _handleAgentEvent?: AwaitableSessionEventHandler; + _disconnectFromAgent?: () => void; + _reconnectToAgent?: () => void; _extensionRunner?: { hasHandlers?: (eventType: string) => boolean; }; @@ -708,23 +710,39 @@ export function installSessionEventWriteLock(params: { }): void { installAwaitableSessionEventQueue(params.session); const session = params.session as SessionEventProcessor; - const original = session["_processAgentEvent"]; + const original = session["_handleAgentEvent"]; if ( typeof original !== "function" || session["__openclawSessionEventWriteLockInstalled"] === true ) { return; } + + const canReconnect = + typeof session["_disconnectFromAgent"] === "function" && + typeof session["_reconnectToAgent"] === "function"; + if (canReconnect) { + session["_disconnectFromAgent"]?.(); + } + session["__openclawSessionEventWriteLockInstalled"] = true; - session["_processAgentEvent"] = async function lockedProcessAgentEvent( + const wrapped: AwaitableSessionEventHandler = async function lockedSessionEventHandler( this: unknown, event: unknown, + signal?: unknown, ) { if (!eventMayReachTranscriptWriters(session, event)) { - return await original.call(this, event); + return await original.call(this, event, signal); } - return await params.withSessionWriteLock(async () => await original.call(this, event)); + return await params.withSessionWriteLock(async () => await original.call(this, event, signal)); }; + wrapped["__openclawSessionEventQueueAwaitInstalled"] = + original["__openclawSessionEventQueueAwaitInstalled"]; + session["_handleAgentEvent"] = wrapped; + + if (canReconnect) { + session["_reconnectToAgent"]?.(); + } } export function installSessionExternalHookWriteLock(params: { diff --git a/src/agents/pi-embedded-runner/run/attempt.sessions-yield.ts b/src/agents/embedded-agent-runner/run/attempt.sessions-yield.ts similarity index 97% rename from src/agents/pi-embedded-runner/run/attempt.sessions-yield.ts rename to src/agents/embedded-agent-runner/run/attempt.sessions-yield.ts index 9f3d170b56f..0a7cafeb068 100644 --- a/src/agents/pi-embedded-runner/run/attempt.sessions-yield.ts +++ b/src/agents/embedded-agent-runner/run/attempt.sessions-yield.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../../runtime/index.js"; import { log } from "../logger.js"; const SESSIONS_YIELD_INTERRUPT_CUSTOM_TYPE = "openclaw.sessions_yield_interrupt"; @@ -43,7 +43,7 @@ export async function waitForSessionsYieldAbortSettle(params: { } } -// Return a synthetic aborted response so pi-agent-core unwinds without a real provider call. +// Return a synthetic aborted response so agent runtime unwinds without a real provider call. export function createYieldAbortedResponse(model: { api?: string; provider?: string; @@ -103,7 +103,7 @@ export function createYieldAbortedResponse(model: { }; } -// Queue a hidden steering message so pi-agent-core injects it before the next +// Queue a hidden steering message so agent runtime injects it before the next // LLM call once the current assistant turn finishes executing its tool calls. export function queueSessionsYieldInterruptMessage(activeSession: { agent: { steer: (message: AgentMessage) => void }; diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-marker.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.bootstrap-marker.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-marker.test.ts rename to src/agents/embedded-agent-runner/run/attempt.spawn-workspace.bootstrap-marker.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts rename to src/agents/embedded-agent-runner/run/attempt.spawn-workspace.bootstrap-routing.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts rename to src/agents/embedded-agent-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.cache-ttl.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.cache-ttl.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.spawn-workspace.cache-ttl.test.ts rename to src/agents/embedded-agent-runner/run/attempt.spawn-workspace.cache-ttl.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts similarity index 93% rename from src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts rename to src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts index ce1d1ea62cd..27664d2f7b5 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-engine.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { HEARTBEAT_TRANSCRIPT_PROMPT } from "../../../auto-reply/heartbeat.js"; import type { OpenClawConfig } from "../../../config/types.js"; @@ -26,7 +26,6 @@ import { createDefaultEmbeddedSession, createContextEngineBootstrapAndAssemble, createContextEngineAttemptRunner, - createSubscriptionMock, expectCalledWithSessionKey, getHoisted, resetEmbeddedAttemptHarness, @@ -212,33 +211,7 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { vi.restoreAllMocks(); }); - it("flushes block replies again after compaction retry wait resolves", async () => { - const order: string[] = []; - let flushCount = 0; - const onBlockReplyFlush = vi.fn(async () => { - flushCount += 1; - order.push(`flush-${flushCount}`); - }); - hoisted.waitForCompactionRetryWithAggregateTimeoutMock.mockImplementation(async () => { - order.push("retry-wait"); - return { timedOut: false }; - }); - - await createContextEngineAttemptRunner({ - contextEngine: createContextEngineBootstrapAndAssemble(), - sessionKey, - tempPaths, - attemptOverrides: { - onBlockReplyFlush, - }, - }); - - expect(onBlockReplyFlush).toHaveBeenCalledTimes(2); - expect(hoisted.waitForCompactionRetryWithAggregateTimeoutMock).toHaveBeenCalledTimes(1); - expect(order).toEqual(["flush-1", "retry-wait", "flush-2"]); - }); - - it("enables Tool Search controls for embedded PI runs when configured", async () => { + it("enables Tool Search controls for embedded OpenClaw runs when configured", async () => { await createContextEngineAttemptRunner({ contextEngine: { assemble: async ({ messages }) => ({ messages, estimatedTokens: 1 }), @@ -1601,102 +1574,6 @@ describe("runEmbeddedAttempt context engine sessionKey forwarding", () => { expect(events.indexOf("bootstrap")).toBeLessThan(events.indexOf("lock")); }); - it("does not acquire the session write lock after aborting during prep", async () => { - const abortController = new AbortController(); - hoisted.resolveBootstrapContextForRunMock.mockImplementation(async () => { - abortController.abort(); - return { bootstrapFiles: [], contextFiles: [] }; - }); - - await expect( - createContextEngineAttemptRunner({ - contextEngine: createContextEngineBootstrapAndAssemble(), - sessionKey, - tempPaths, - attemptOverrides: { abortSignal: abortController.signal }, - }), - ).rejects.toMatchObject({ name: "AbortError" }); - - expect(hoisted.acquireSessionWriteLockMock).not.toHaveBeenCalled(); - }); - - it("disposes prep runtimes after aborting before the session write lock", async () => { - const abortController = new AbortController(); - const lspDispose = vi.fn(async () => {}); - hoisted.createBundleLspToolRuntimeMock.mockImplementationOnce(async () => { - abortController.abort(); - return { - tools: [], - dispose: lspDispose, - }; - }); - - await expect( - createContextEngineAttemptRunner({ - contextEngine: createContextEngineBootstrapAndAssemble(), - sessionKey, - tempPaths, - attemptOverrides: { - abortSignal: abortController.signal, - disableTools: false, - toolsAllow: ["*"], - }, - }), - ).rejects.toMatchObject({ name: "AbortError" }); - - expect(hoisted.acquireSessionWriteLockMock).not.toHaveBeenCalled(); - expect(hoisted.createBundleLspToolRuntimeMock).toHaveBeenCalledTimes(1); - expect(lspDispose).toHaveBeenCalledTimes(1); - }); - - it("stops session setup when aborting after the session write lock", async () => { - const abortController = new AbortController(); - const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble(); - hoisted.sessionManagerOpenMock.mockImplementationOnce(() => { - abortController.abort(); - return hoisted.sessionManager; - }); - - await expect( - createContextEngineAttemptRunner({ - contextEngine: createTestContextEngine({ bootstrap, assemble }), - sessionKey, - tempPaths, - attemptOverrides: { abortSignal: abortController.signal }, - }), - ).rejects.toMatchObject({ name: "AbortError" }); - - expect(hoisted.acquireSessionWriteLockMock).toHaveBeenCalledTimes(1); - expect(bootstrap).not.toHaveBeenCalled(); - expect(assemble).not.toHaveBeenCalled(); - expect(hoisted.createAgentSessionMock).not.toHaveBeenCalled(); - }); - - it("does not submit a prompt after aborting a created session", async () => { - const abortController = new AbortController(); - const sessionPrompt = vi.fn(async () => {}); - hoisted.subscribeEmbeddedPiSessionMock.mockImplementationOnce(() => { - abortController.abort(); - return createSubscriptionMock(); - }); - - await expect( - createContextEngineAttemptRunner({ - contextEngine: createContextEngineBootstrapAndAssemble(), - sessionKey, - tempPaths, - createSession: () => - createDefaultEmbeddedSession({ - initialMessages: [seedMessage], - prompt: sessionPrompt, - }), - attemptOverrides: { abortSignal: abortController.signal }, - }), - ).rejects.toMatchObject({ name: "AbortError" }); - - expect(sessionPrompt).not.toHaveBeenCalled(); - }); - it("forwards modelId to assemble", async () => { const { bootstrap, assemble } = createContextEngineBootstrapAndAssemble(); const contextEngine = createTestContextEngine({ bootstrap, assemble }); @@ -2107,7 +1984,7 @@ describe("runEmbeddedAttempt context engine mid-turn precheck integration", () = expect(loopHookParams.midTurnPrecheck).toBeUndefined(); }); - it("recovers when Pi persists the mid-turn precheck as an assistant error", async () => { + it("recovers when the runtime persists the mid-turn precheck as an assistant error", async () => { hoisted.installToolResultContextGuardMock.mockImplementation((...args: unknown[]) => { const params = args[0] as ToolResultGuardInstallParams; params.midTurnPrecheck?.onMidTurnPrecheck?.({ @@ -2121,7 +1998,7 @@ describe("runEmbeddedAttempt context engine mid-turn precheck integration", () = return () => {}; }); - const syntheticPiError = { + const syntheticRuntimeError = { role: "assistant", content: [{ type: "text", text: "" }], stopReason: "error", @@ -2147,7 +2024,7 @@ describe("runEmbeddedAttempt context engine mid-turn precheck integration", () = }, sessionMessages: [seedMessage], sessionPrompt: async (session) => { - session.messages = [...session.messages, syntheticPiError]; + session.messages = [...session.messages, syntheticRuntimeError]; }, }); diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-injection.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-injection.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-injection.test.ts rename to src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-injection.test.ts index 679cd583284..c1f2821cdb9 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-injection.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.context-injection.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { filterHeartbeatTranscriptArtifacts } from "../../../auto-reply/heartbeat-filter.js"; import { HEARTBEAT_PROMPT } from "../../../auto-reply/heartbeat.js"; diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.resource-loader.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.resource-loader.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.spawn-workspace.resource-loader.test.ts rename to src/agents/embedded-agent-runner/run/attempt.spawn-workspace.resource-loader.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.sessions-spawn.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.sessions-spawn.test.ts similarity index 86% rename from src/agents/pi-embedded-runner/run/attempt.spawn-workspace.sessions-spawn.test.ts rename to src/agents/embedded-agent-runner/run/attempt.spawn-workspace.sessions-spawn.test.ts index 81704b34ac5..bdfd7f40167 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.sessions-spawn.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.sessions-spawn.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from "vitest"; -import { createPiToolsSandboxContext } from "../../test-helpers/pi-tools-sandbox-context.js"; +import { createAgentToolsSandboxContext } from "../../test-helpers/agent-tools-sandbox-context.js"; import { resolveAttemptSpawnWorkspaceDir } from "./attempt.thread-helpers.js"; describe("runEmbeddedAttempt sessions_spawn workspace inheritance", () => { it("passes the real workspace to sessions_spawn when workspaceAccess is ro", () => { const realWorkspace = "/tmp/openclaw-real-workspace"; const sandboxWorkspace = "/tmp/openclaw-sandbox-workspace"; - const sandbox = createPiToolsSandboxContext({ + const sandbox = createAgentToolsSandboxContext({ workspaceDir: sandboxWorkspace, agentWorkspaceDir: realWorkspace, workspaceAccess: "ro", @@ -24,7 +24,7 @@ describe("runEmbeddedAttempt sessions_spawn workspace inheritance", () => { it("does not override spawned workspace when sandbox workspace is rw", () => { const realWorkspace = "/tmp/openclaw-real-workspace"; - const sandbox = createPiToolsSandboxContext({ + const sandbox = createAgentToolsSandboxContext({ workspaceDir: realWorkspace, agentWorkspaceDir: realWorkspace, workspaceAccess: "rw", diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.test-support.ts similarity index 92% rename from src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts rename to src/agents/embedded-agent-runner/run/attempt.spawn-workspace.test-support.ts index 93fbeaea7e2..afe28942cfd 100644 --- a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.test-support.ts +++ b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.test-support.ts @@ -1,8 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { expect, vi, type Mock } from "vitest"; import type { AssembleResult, @@ -19,23 +18,22 @@ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, } from "../../../shared/string-coerce.js"; -import type { EmbeddedContextFile } from "../../pi-embedded-helpers.js"; +import type { EmbeddedContextFile } from "../../embedded-agent-helpers.js"; import type { MessagingToolSend, MessagingToolSourceReplyPayload, -} from "../../pi-embedded-messaging.types.js"; +} from "../../embedded-agent-messaging.types.js"; +import type { AgentMessage } from "../../runtime/index.js"; import type { WorkspaceBootstrapFile } from "../../workspace.js"; -type SubscribeEmbeddedPiSessionFn = - typeof import("../../pi-embedded-subscribe.js").subscribeEmbeddedPiSession; +type SubscribeEmbeddedAgentSessionFn = + typeof import("../../embedded-agent-subscribe.js").subscribeEmbeddedAgentSession; type AcquireSessionWriteLockFn = typeof import("../../session-write-lock.js").acquireSessionWriteLock; type ShouldPreemptivelyCompactBeforePromptFn = typeof import("./preemptive-compaction.js").shouldPreemptivelyCompactBeforePrompt; -type WaitForCompactionRetryWithAggregateTimeoutFn = - typeof import("./compaction-retry-aggregate-timeout.js").waitForCompactionRetryWithAggregateTimeout; -type SubscriptionMock = ReturnType; +type SubscriptionMock = ReturnType; type UnknownMock = Mock<(...args: unknown[]) => unknown>; type AsyncUnknownMock = Mock<(...args: unknown[]) => Promise>; type AsyncContextEngineMaintenanceMock = Mock< @@ -70,10 +68,7 @@ type AttemptSpawnWorkspaceHoisted = { ensureGlobalUndiciStreamTimeoutsMock: UnknownMock; buildEmbeddedMessageActionDiscoveryInputMock: UnknownMock; createOpenClawCodingToolsMock: UnknownMock; - getOrCreateSessionMcpRuntimeMock: AsyncUnknownMock; - materializeBundleMcpToolsForRunMock: AsyncUnknownMock; - createBundleLspToolRuntimeMock: AsyncUnknownMock; - subscribeEmbeddedPiSessionMock: Mock; + subscribeEmbeddedAgentSessionMock: Mock; acquireSessionWriteLockMock: Mock; installToolResultContextGuardMock: UnknownMock; installContextEngineLoopHookMock: UnknownMock; @@ -94,7 +89,6 @@ type AttemptSpawnWorkspaceHoisted = { (sessionKey: string | undefined, config: unknown) => number | undefined >; limitHistoryTurnsMock: Mock<(messages: T, limit: number | undefined) => T>; - waitForCompactionRetryWithAggregateTimeoutMock: Mock; preemptiveCompactionCalls: Parameters[0][]; systemPromptOverrideTexts: string[]; sessionManager: SessionManagerMocks; @@ -144,13 +138,10 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => { const ensureGlobalUndiciStreamTimeoutsMock = vi.fn(); const buildEmbeddedMessageActionDiscoveryInputMock = vi.fn((params: unknown) => params); const createOpenClawCodingToolsMock = vi.fn(() => []); - const getOrCreateSessionMcpRuntimeMock = vi.fn(async () => undefined); - const materializeBundleMcpToolsForRunMock = vi.fn(async () => undefined); - const createBundleLspToolRuntimeMock = vi.fn(async () => undefined); const installToolResultContextGuardMock = vi.fn(() => () => {}); const installContextEngineLoopHookMock = vi.fn(() => () => {}); const flushPendingToolResultsAfterIdleMock = vi.fn(async () => {}); - const subscribeEmbeddedPiSessionMock = vi.fn(() => + const subscribeEmbeddedAgentSessionMock = vi.fn(() => createSubscriptionMock(), ); const acquireSessionWriteLockMock = vi.fn(async (_params) => ({ @@ -194,10 +185,6 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => { const limitHistoryTurnsMock = vi.fn<(messages: T, limit: number | undefined) => T>( (messages) => messages, ); - const waitForCompactionRetryWithAggregateTimeoutMock = - vi.fn(async () => ({ - timedOut: false, - })); const preemptiveCompactionCalls: Parameters[0][] = []; const systemPromptOverrideTexts: string[] = []; const sessionManager = { @@ -219,10 +206,7 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => { ensureGlobalUndiciStreamTimeoutsMock, buildEmbeddedMessageActionDiscoveryInputMock, createOpenClawCodingToolsMock, - getOrCreateSessionMcpRuntimeMock, - materializeBundleMcpToolsForRunMock, - createBundleLspToolRuntimeMock, - subscribeEmbeddedPiSessionMock, + subscribeEmbeddedAgentSessionMock, acquireSessionWriteLockMock, installToolResultContextGuardMock, installContextEngineLoopHookMock, @@ -241,7 +225,6 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => { detectAndLoadPromptImagesMock, getHistoryLimitFromSessionKeyMock, limitHistoryTurnsMock, - waitForCompactionRetryWithAggregateTimeoutMock, preemptiveCompactionCalls, systemPromptOverrideTexts, sessionManager, @@ -302,7 +285,7 @@ vi.mock("../../../trajectory/metadata.js", () => ({ buildTrajectoryRunMetadata: () => ({ source: "test" }), })); -vi.mock("@earendil-works/pi-coding-agent", () => { +vi.mock("../../sessions/index.js", () => { function AuthStorage() {} class DefaultResourceLoader { async reload() {} @@ -337,9 +320,9 @@ vi.mock("../../session-tool-result-guard-wrapper.js", () => ({ guardSessionManager: (sessionManager: unknown) => sessionManager, })); -vi.mock("../../pi-embedded-subscribe.js", () => ({ - subscribeEmbeddedPiSession: (params: Parameters[0]) => - hoisted.subscribeEmbeddedPiSessionMock(params), +vi.mock("../../embedded-agent-subscribe.js", () => ({ + subscribeEmbeddedAgentSession: (params: Parameters[0]) => + hoisted.subscribeEmbeddedAgentSessionMock(params), })); vi.mock("../../../plugins/hook-runner-global.js", () => ({ @@ -407,10 +390,23 @@ vi.mock("../../docs-path.js", () => ({ resolveOpenClawReferencePaths: async () => ({ docsPath: undefined, sourcePath: undefined }), })); -vi.mock("../../pi-project-settings.js", () => ({ - createPreparedEmbeddedPiSettingsManager: () => ({ +vi.mock("../../agent-project-settings.js", () => ({ + createPreparedEmbeddedAgentSettingsManager: () => ({ + reload: async () => {}, getCompactionReserveTokens: () => 0, getCompactionKeepRecentTokens: () => 40_000, + getDefaultProvider: () => undefined, + getDefaultModel: () => undefined, + getDefaultThinkingLevel: () => undefined, + getBlockImages: () => false, + getSteeringMode: () => undefined, + getFollowUpMode: () => undefined, + getTransport: () => undefined, + getThinkingBudgets: () => undefined, + getProviderRetrySettings: () => ({ maxRetryDelayMs: undefined }), + getImageAutoResize: () => false, + getShellCommandPrefix: () => undefined, + getShellPath: () => undefined, getGlobalSettings: () => ({}), getProjectSettings: () => ({}), applyOverrides: () => {}, @@ -418,9 +414,9 @@ vi.mock("../../pi-project-settings.js", () => ({ }), })); -vi.mock("../../pi-settings.js", () => ({ - applyPiAutoCompactionGuard: () => {}, - applyPiCompactionSettingsFromConfig: () => ({ +vi.mock("../../agent-settings.js", () => ({ + applyAgentAutoCompactionGuard: () => {}, + applyAgentCompactionSettingsFromConfig: () => ({ didOverride: false, compaction: { reserveTokens: 0, @@ -567,7 +563,7 @@ vi.mock("../../cache-trace.js", () => ({ createCacheTrace: () => undefined, })); -vi.mock("../../pi-tools.js", () => ({ +vi.mock("../../agent-tools.js", () => ({ createOpenClawCodingTools: (options?: { workspaceDir?: string; spawnWorkspaceDir?: string }) => hoisted.createOpenClawCodingToolsMock(options), resolveProcessToolScopeKey: ({ @@ -584,18 +580,15 @@ vi.mock("../../pi-tools.js", () => ({ resolveToolLoopDetectionConfig: () => undefined, })); -vi.mock("../../pi-bundle-mcp-tools.js", () => ({ +vi.mock("../../agent-bundle-mcp-tools.js", () => ({ createBundleMcpToolRuntime: async () => undefined, - getOrCreateSessionMcpRuntime: (...args: unknown[]) => - hoisted.getOrCreateSessionMcpRuntimeMock(...args), - materializeBundleMcpToolsForRun: (...args: unknown[]) => - hoisted.materializeBundleMcpToolsForRunMock(...args), + getOrCreateSessionMcpRuntime: async () => undefined, + materializeBundleMcpToolsForRun: async () => undefined, retireSessionMcpRuntime: async () => true, })); -vi.mock("../../pi-bundle-lsp-runtime.js", () => ({ - createBundleLspToolRuntime: (...args: unknown[]) => - hoisted.createBundleLspToolRuntimeMock(...args), +vi.mock("../../agent-bundle-lsp-runtime.js", () => ({ + createBundleLspToolRuntime: async () => undefined, })); vi.mock("../../../image-generation/runtime.js", () => ({ @@ -801,9 +794,10 @@ vi.mock("../utils.js", () => ({ })); vi.mock("./compaction-retry-aggregate-timeout.js", () => ({ - waitForCompactionRetryWithAggregateTimeout: ( - ...args: Parameters - ) => hoisted.waitForCompactionRetryWithAggregateTimeoutMock(...args), + waitForCompactionRetryWithAggregateTimeout: async () => ({ + timedOut: false, + aborted: false, + }), })); vi.mock("./compaction-timeout.js", () => ({ @@ -903,7 +897,7 @@ export function resetEmbeddedAttemptHarness( params: { includeSpawnSubagent?: boolean; subscribeImpl?: Parameters< - (typeof hoisted.subscribeEmbeddedPiSessionMock)["mockImplementation"] + (typeof hoisted.subscribeEmbeddedAgentSessionMock)["mockImplementation"] >[0]; sessionMessages?: AgentMessage[]; } = {}, @@ -952,10 +946,7 @@ export function resetEmbeddedAttemptHarness( }, ]; }); - hoisted.getOrCreateSessionMcpRuntimeMock.mockReset().mockResolvedValue(undefined); - hoisted.materializeBundleMcpToolsForRunMock.mockReset().mockResolvedValue(undefined); - hoisted.createBundleLspToolRuntimeMock.mockReset().mockResolvedValue(undefined); - hoisted.subscribeEmbeddedPiSessionMock + hoisted.subscribeEmbeddedAgentSessionMock .mockReset() .mockImplementation(() => createSubscriptionMock()); hoisted.acquireSessionWriteLockMock.mockReset().mockResolvedValue({ @@ -985,9 +976,6 @@ export function resetEmbeddedAttemptHarness( hoisted.runContextEngineMaintenanceMock.mockReset().mockResolvedValue(undefined); hoisted.getHistoryLimitFromSessionKeyMock.mockReset().mockReturnValue(undefined); hoisted.limitHistoryTurnsMock.mockReset().mockImplementation((messages) => messages); - hoisted.waitForCompactionRetryWithAggregateTimeoutMock - .mockReset() - .mockResolvedValue({ timedOut: false }); hoisted.preemptiveCompactionCalls.length = 0; hoisted.systemPromptOverrideTexts.length = 0; hoisted.sessionManager.getLeafEntry.mockReset().mockReturnValue(null); @@ -998,7 +986,7 @@ export function resetEmbeddedAttemptHarness( .mockReturnValue({ messages: params.sessionMessages ?? [] }); hoisted.sessionManager.appendCustomEntry.mockReset(); if (params.subscribeImpl) { - hoisted.subscribeEmbeddedPiSessionMock.mockImplementation(params.subscribeImpl); + hoisted.subscribeEmbeddedAgentSessionMock.mockImplementation(params.subscribeImpl); } } @@ -1109,7 +1097,7 @@ export const testModel = { compat: {}, contextWindow: 8192, input: ["text"], -} as unknown as Model; +} as unknown as Model; const testAuthStorage = { getApiKey: async () => undefined, diff --git a/src/agents/pi-embedded-runner/run/attempt.spawn-workspace.timeout.test.ts b/src/agents/embedded-agent-runner/run/attempt.spawn-workspace.timeout.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.spawn-workspace.timeout.test.ts rename to src/agents/embedded-agent-runner/run/attempt.spawn-workspace.timeout.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.stop-reason-recovery.test.ts b/src/agents/embedded-agent-runner/run/attempt.stop-reason-recovery.test.ts similarity index 93% rename from src/agents/pi-embedded-runner/run/attempt.stop-reason-recovery.test.ts rename to src/agents/embedded-agent-runner/run/attempt.stop-reason-recovery.test.ts index 1bd28bf1df3..980bc3711b6 100644 --- a/src/agents/pi-embedded-runner/run/attempt.stop-reason-recovery.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.stop-reason-recovery.test.ts @@ -1,5 +1,9 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { createAssistantMessageEventStream, type Context, type Model } from "@earendil-works/pi-ai"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; +import { + createAssistantMessageEventStream, + type Context, + type Model, +} from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { wrapStreamFnHandleSensitiveStopReason } from "./attempt.stop-reason-recovery.js"; diff --git a/src/agents/pi-embedded-runner/run/attempt.stop-reason-recovery.ts b/src/agents/embedded-agent-runner/run/attempt.stop-reason-recovery.ts similarity index 98% rename from src/agents/pi-embedded-runner/run/attempt.stop-reason-recovery.ts rename to src/agents/embedded-agent-runner/run/attempt.stop-reason-recovery.ts index 792a8961228..196e461946c 100644 --- a/src/agents/pi-embedded-runner/run/attempt.stop-reason-recovery.ts +++ b/src/agents/embedded-agent-runner/run/attempt.stop-reason-recovery.ts @@ -1,6 +1,6 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { createAssistantMessageEventStream, streamSimple } from "@earendil-works/pi-ai"; +import { createAssistantMessageEventStream, streamSimple } from "openclaw/plugin-sdk/llm"; import { formatErrorMessage } from "../../../infra/errors.js"; +import type { StreamFn } from "../../runtime/index.js"; import { createStreamIteratorWrapper } from "../../stream-iterator-wrapper.js"; import { buildStreamErrorAssistantMessage } from "../../stream-message-shared.js"; diff --git a/src/agents/pi-embedded-runner/run/attempt.subscription-cleanup.test.ts b/src/agents/embedded-agent-runner/run/attempt.subscription-cleanup.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.subscription-cleanup.test.ts rename to src/agents/embedded-agent-runner/run/attempt.subscription-cleanup.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.subscription-cleanup.ts b/src/agents/embedded-agent-runner/run/attempt.subscription-cleanup.ts similarity index 94% rename from src/agents/pi-embedded-runner/run/attempt.subscription-cleanup.ts rename to src/agents/embedded-agent-runner/run/attempt.subscription-cleanup.ts index e83f02c3b00..598472411a6 100644 --- a/src/agents/pi-embedded-runner/run/attempt.subscription-cleanup.ts +++ b/src/agents/embedded-agent-runner/run/attempt.subscription-cleanup.ts @@ -1,4 +1,4 @@ -import type { SubscribeEmbeddedPiSessionParams } from "../../pi-embedded-subscribe.types.js"; +import type { SubscribeEmbeddedAgentSessionParams } from "../../embedded-agent-subscribe.types.js"; import { log } from "../logger.js"; export const EMBEDDED_ABORT_SETTLE_TIMEOUT_MS = @@ -47,8 +47,8 @@ async function waitForEmbeddedAbortSettle(params: { } export function buildEmbeddedSubscriptionParams( - params: SubscribeEmbeddedPiSessionParams, -): SubscribeEmbeddedPiSessionParams { + params: SubscribeEmbeddedAgentSessionParams, +): SubscribeEmbeddedAgentSessionParams { return params; } diff --git a/src/agents/pi-embedded-runner/run/attempt.test.ts b/src/agents/embedded-agent-runner/run/attempt.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/attempt.test.ts rename to src/agents/embedded-agent-runner/run/attempt.test.ts index e91e22a646f..483f984e98c 100644 --- a/src/agents/pi-embedded-runner/run/attempt.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.test.ts @@ -1,4 +1,4 @@ -import { streamSimple } from "@earendil-works/pi-ai"; +import { streamSimple } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; vi.mock("../context-engine-capabilities.js", () => ({ diff --git a/src/agents/pi-embedded-runner/run/attempt.thread-helpers.ts b/src/agents/embedded-agent-runner/run/attempt.thread-helpers.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.thread-helpers.ts rename to src/agents/embedded-agent-runner/run/attempt.thread-helpers.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.test.ts b/src/agents/embedded-agent-runner/run/attempt.tool-call-argument-repair.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.test.ts rename to src/agents/embedded-agent-runner/run/attempt.tool-call-argument-repair.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts b/src/agents/embedded-agent-runner/run/attempt.tool-call-argument-repair.ts similarity index 98% rename from src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts rename to src/agents/embedded-agent-runner/run/attempt.tool-call-argument-repair.ts index 896112a8e7c..cb650b86098 100644 --- a/src/agents/pi-embedded-runner/run/attempt.tool-call-argument-repair.ts +++ b/src/agents/embedded-agent-runner/run/attempt.tool-call-argument-repair.ts @@ -1,7 +1,7 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { streamSimple } from "@earendil-works/pi-ai"; +import { streamSimple } from "openclaw/plugin-sdk/llm"; import { extractBalancedJsonPrefix } from "../../../shared/balanced-json.js"; import { normalizeProviderId } from "../../model-selection.js"; +import type { StreamFn } from "../../runtime/index.js"; import { log } from "../logger.js"; import { createHtmlEntityToolCallArgumentDecodingWrapper, diff --git a/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.test.ts b/src/agents/embedded-agent-runner/run/attempt.tool-call-normalization.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.test.ts rename to src/agents/embedded-agent-runner/run/attempt.tool-call-normalization.test.ts index e5839dd86a5..6e217d5f544 100644 --- a/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.test.ts +++ b/src/agents/embedded-agent-runner/run/attempt.tool-call-normalization.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; import { sanitizeReplayToolCallIdsForStream, diff --git a/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts b/src/agents/embedded-agent-runner/run/attempt.tool-call-normalization.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts rename to src/agents/embedded-agent-runner/run/attempt.tool-call-normalization.ts index 03782c0b622..9e5bac419f7 100644 --- a/src/agents/pi-embedded-runner/run/attempt.tool-call-normalization.ts +++ b/src/agents/embedded-agent-runner/run/attempt.tool-call-normalization.ts @@ -1,9 +1,8 @@ -import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core"; -import { streamSimple } from "@earendil-works/pi-ai"; +import { streamSimple } from "openclaw/plugin-sdk/llm"; import { visitObjectContentBlocks } from "../../../shared/message-content-blocks.js"; import { normalizeLowercaseStringOrEmpty } from "../../../shared/string-coerce.js"; -import { normalizeStringEntries } from "../../../shared/string-normalization.js"; -import { validateAnthropicTurns, validateGeminiTurns } from "../../pi-embedded-helpers.js"; +import { validateAnthropicTurns, validateGeminiTurns } from "../../embedded-agent-helpers.js"; +import type { AgentMessage, StreamFn } from "../../runtime/index.js"; import { sanitizeToolUseResultPairing } from "../../session-transcript-repair.js"; import { extractToolCallsFromAssistant, @@ -89,7 +88,10 @@ function buildStructuredToolNameCandidates(rawName: string): string[] { addCandidate(normalizedDelimiter); addCandidate(normalizeToolName(normalizedDelimiter)); - const segments = normalizeStringEntries(normalizedDelimiter.split(".")); + const segments = normalizedDelimiter + .split(".") + .map((segment) => segment.trim()) + .filter(Boolean); if (segments.length > 1) { for (let index = 1; index < segments.length; index += 1) { const suffix = segments.slice(index).join("."); diff --git a/src/agents/pi-embedded-runner/run/attempt.tool-run-context.ts b/src/agents/embedded-agent-runner/run/attempt.tool-run-context.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.tool-run-context.ts rename to src/agents/embedded-agent-runner/run/attempt.tool-run-context.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.transcript-policy.test.ts b/src/agents/embedded-agent-runner/run/attempt.transcript-policy.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.transcript-policy.test.ts rename to src/agents/embedded-agent-runner/run/attempt.transcript-policy.test.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.transcript-policy.ts b/src/agents/embedded-agent-runner/run/attempt.transcript-policy.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/attempt.transcript-policy.ts rename to src/agents/embedded-agent-runner/run/attempt.transcript-policy.ts diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/embedded-agent-runner/run/attempt.ts similarity index 96% rename from src/agents/pi-embedded-runner/run/attempt.ts rename to src/agents/embedded-agent-runner/run/attempt.ts index 275296b118b..449ee9e7121 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/embedded-agent-runner/run/attempt.ts @@ -1,9 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage } from "@earendil-works/pi-ai"; -import { createAgentSession, SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { isAcpRuntimeSpawnAvailable } from "../../../acp/runtime/availability.js"; import { buildHierarchyReinforcementMessage } from "../../../auto-reply/handoff-summarizer.js"; import { filterHeartbeatTranscriptArtifacts } from "../../../auto-reply/heartbeat-filter.js"; @@ -21,7 +19,7 @@ import { } from "../../../config/sessions/transcript-write-context.js"; import { assertContextEngineHostSupport, - PI_EMBEDDED_CONTEXT_ENGINE_HOST, + OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST, } from "../../../context-engine/host-compat.js"; import { resolveContextEngineOwnerPluginId } from "../../../context-engine/registry.js"; import type { AssembleResult } from "../../../context-engine/types.js"; @@ -36,6 +34,7 @@ import { isEmbeddedMode } from "../../../infra/embedded-mode.js"; import { formatErrorMessage } from "../../../infra/errors.js"; import { resolveHeartbeatSummaryForAgent } from "../../../infra/heartbeat-summary.js"; import { getMachineDisplayName } from "../../../infra/machine-name.js"; +import { createCodexNativeWebSearchWrapper } from "../../../llm/providers/stream-wrappers/openai.js"; import { MAX_IMAGE_BYTES } from "../../../media/constants.js"; import { listRegisteredPluginAgentPromptGuidance } from "../../../plugins/command-registry-state.js"; import { getCurrentPluginMetadataSnapshot } from "../../../plugins/current-plugin-metadata-snapshot.js"; @@ -66,7 +65,35 @@ import { import { resolveUserPath } from "../../../utils.js"; import { normalizeMessageChannel } from "../../../utils/message-channel.js"; import { isReasoningTagProvider } from "../../../utils/provider-utils.js"; +import { createBundleLspToolRuntime } from "../../agent-bundle-lsp-runtime.js"; +import { + getOrCreateSessionMcpRuntime, + materializeBundleMcpToolsForRun, +} from "../../agent-bundle-mcp-tools.js"; +import { createPreparedEmbeddedAgentSettingsManager } from "../../agent-project-settings.js"; import { resolveAgentDir, resolveSessionAgentIds } from "../../agent-scope.js"; +import { + applyAgentAutoCompactionGuard, + applyAgentCompactionSettingsFromConfig, + isSilentOverflowProneModel, + resolveEffectiveCompactionMode, +} from "../../agent-settings.js"; +import { + createClientToolNameConflictError, + findClientToolNameConflicts, + toClientToolDefinitions, +} from "../../agent-tool-definition-adapter.js"; +import { + createOpenClawCodingTools, + resolveProcessToolScopeKey, + resolveToolLoopDetectionConfig, +} from "../../agent-tools.js"; +import { + resolveEffectiveToolPolicy, + resolveGroupToolPolicy, + resolveInheritedToolPolicyForSession, + resolveSubagentToolPolicyForSession, +} from "../../agent-tools.policy.js"; import { createAnthropicPayloadLogger } from "../../anthropic-payload-log.js"; import { listActiveProcessSessionReferences } from "../../bash-process-references.js"; import { @@ -101,6 +128,17 @@ import { } from "../../code-mode.js"; import { DEFAULT_CONTEXT_TOKENS } from "../../defaults.js"; import { resolveOpenClawReferencePaths } from "../../docs-path.js"; +import type { EmbeddedContextFile } from "../../embedded-agent-helpers.js"; +import { + downgradeOpenAIFunctionCallReasoningPairs, + downgradeOpenAIReasoningBlocks, + isCloudCodeAssistFormatError, + resolveBootstrapMaxChars, + resolveBootstrapPromptTruncationWarningMode, + resolveBootstrapTotalMaxChars, +} from "../../embedded-agent-helpers.js"; +import { countActiveToolExecutions } from "../../embedded-agent-subscribe.handlers.tools.js"; +import { subscribeEmbeddedAgentSession } from "../../embedded-agent-subscribe.js"; import { isTimeoutError } from "../../failover-error.js"; import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js"; import { resolveImageSanitizationLimits } from "../../image-sanitization.js"; @@ -109,46 +147,6 @@ import { filterLocalModelLeanTools, isLocalModelLeanEnabled } from "../../local- import { resolveModelAuthMode } from "../../model-auth.js"; import { resolveDefaultModelForAgent } from "../../model-selection.js"; import { supportsModelTools } from "../../model-tool-support.js"; -import { createBundleLspToolRuntime } from "../../pi-bundle-lsp-runtime.js"; -import { - getOrCreateSessionMcpRuntime, - materializeBundleMcpToolsForRun, -} from "../../pi-bundle-mcp-tools.js"; -import type { EmbeddedContextFile } from "../../pi-embedded-helpers.js"; -import { - downgradeOpenAIFunctionCallReasoningPairs, - downgradeOpenAIReasoningBlocks, - isCloudCodeAssistFormatError, - resolveBootstrapMaxChars, - resolveBootstrapPromptTruncationWarningMode, - resolveBootstrapTotalMaxChars, -} from "../../pi-embedded-helpers.js"; -import { countActiveToolExecutions } from "../../pi-embedded-subscribe.handlers.tools.js"; -import { subscribeEmbeddedPiSession } from "../../pi-embedded-subscribe.js"; -import { isCoreToolResultMediaTrustedName } from "../../pi-embedded-subscribe.tools.js"; -import { createPreparedEmbeddedPiSettingsManager } from "../../pi-project-settings.js"; -import { - applyPiAutoCompactionGuard, - applyPiCompactionSettingsFromConfig, - isSilentOverflowProneModel, - resolveEffectiveCompactionMode, -} from "../../pi-settings.js"; -import { - createClientToolNameConflictError, - findClientToolNameConflicts, - toClientToolDefinitions, -} from "../../pi-tool-definition-adapter.js"; -import { - createOpenClawCodingTools, - resolveProcessToolScopeKey, - resolveToolLoopDetectionConfig, -} from "../../pi-tools.js"; -import { - resolveEffectiveToolPolicy, - resolveGroupToolPolicy, - resolveInheritedToolPolicyForSession, - resolveSubagentToolPolicyForSession, -} from "../../pi-tools.policy.js"; import { wrapStreamFnTextTransforms } from "../../plugin-text-transforms.js"; import { resolveAgentPromptSurfaceForSessionKey } from "../../prompt-surface.js"; import { describeProviderRequestRoutingSummary } from "../../provider-attribution.js"; @@ -159,6 +157,7 @@ import { logAgentRuntimeToolDiagnostics, normalizeAgentRuntimeTools, } from "../../runtime-plan/tools.js"; +import type { AgentMessage } from "../../runtime/index.js"; import { resolveSandboxContext } from "../../sandbox.js"; import { resolveSandboxRuntimeStatus } from "../../sandbox/runtime-status.js"; import { repairSessionFileIfNeeded } from "../../session-file-repair.js"; @@ -172,6 +171,7 @@ import { resolveSessionLockMaxHoldFromTimeout, resolveSessionWriteLockOptions, } from "../../session-write-lock.js"; +import { createAgentSession, SessionManager } from "../../sessions/index.js"; import { detectRuntimeShell } from "../../shell-utils.js"; import { applySkillEnvOverrides, @@ -233,7 +233,6 @@ import { prepareGooglePromptCacheStreamFn } from "../google-prompt-cache.js"; import { getHistoryLimitFromSessionKey, limitHistoryTurns } from "../history.js"; import { log } from "../logger.js"; import { buildEmbeddedMessageActionDiscoveryInput } from "../message-action-discovery-input.js"; -import { createCodexNativeWebSearchWrapper } from "../openai-stream-wrappers.js"; import { collectPromptCacheToolNames, beginPromptCacheObservation, @@ -248,10 +247,10 @@ import { validateReplayTurns, } from "../replay-history.js"; import { observeReplayMetadata, replayMetadataFromState } from "../replay-state.js"; -import { createEmbeddedPiResourceLoader } from "../resource-loader.js"; +import { createEmbeddedAgentResourceLoader } from "../resource-loader.js"; import { clearActiveEmbeddedRun, - type EmbeddedPiQueueHandle, + type EmbeddedAgentQueueHandle, setActiveEmbeddedRun, updateActiveEmbeddedRunSessionFile, updateActiveEmbeddedRunSnapshot, @@ -277,7 +276,7 @@ import { collectAllowedToolNames, collectCoreBuiltinToolNames, collectRegisteredToolNames, - PI_RESERVED_TOOL_NAMES, + AGENT_RESERVED_TOOL_NAMES, toSessionToolAllowlist, } from "../tool-name-allowlist.js"; import { @@ -520,7 +519,6 @@ function logRuntimeToolSchemaQuarantine(params: { `[tools] quarantined ${params.diagnostics.length} unsupported tool schema${params.diagnostics.length === 1 ? "" : "s"} before model runtime projection: ${summary}. Run openclaw doctor for details.`, ); } - const MAX_BTW_SNAPSHOT_MESSAGES = 100; const TOOL_SEARCH_CONTROL_ALLOWLIST_NAMES = [ TOOL_SEARCH_CODE_MODE_TOOL_NAME, @@ -759,7 +757,7 @@ function summarizeSessionContext(messages: AgentMessage[]): { }; } -export type EmbeddedPiActiveSessionSteerTarget = { +export type EmbeddedAgentActiveSessionSteerTarget = { agent?: unknown; getSteeringMessages?(): readonly string[]; steer(text: string): Promise; @@ -823,7 +821,7 @@ function isCompactionStartEvent(event: unknown): boolean { ); } -function getPiSteeringQueueMessages(agent: unknown): unknown[] | undefined { +function getAgentSteeringQueueMessages(agent: unknown): unknown[] | undefined { if (!agent || typeof agent !== "object") { return undefined; } @@ -836,14 +834,14 @@ function getPiSteeringQueueMessages(agent: unknown): unknown[] | undefined { } async function cancelQueuedSteeringMessage( - activeSession: EmbeddedPiActiveSessionSteerTarget, + activeSession: EmbeddedAgentActiveSessionSteerTarget, text: string, ): Promise { - const queuedMessages = getPiSteeringQueueMessages(activeSession.agent); + const queuedMessages = getAgentSteeringQueueMessages(activeSession.agent); if (!queuedMessages) { return false; } - // Pi exposes only all-queue clears publicly; mutate the exact pending message + // The session runtime exposes only all-queue clears publicly; mutate the exact pending message // so unrelated queued messages keep their full payloads. const queueIndex = queuedMessages.findIndex( (message) => extractQueuedUserMessageText(message) === text, @@ -892,7 +890,7 @@ function resolveAttemptStreamAuthProfileId( } async function steerAndWaitForTranscriptCommit( - activeSession: EmbeddedPiActiveSessionSteerTarget, + activeSession: EmbeddedAgentActiveSessionSteerTarget, text: string, timeoutMs: number, ): Promise { @@ -980,7 +978,7 @@ async function steerAndWaitForTranscriptCommit( } async function steerActiveSessionWithOptionalDeliveryWait( - activeSession: EmbeddedPiActiveSessionSteerTarget, + activeSession: EmbeddedAgentActiveSessionSteerTarget, text: string, options: { deliveryTimeoutMs?: number; waitForTranscriptCommit?: boolean } | undefined, ): Promise { @@ -1272,21 +1270,6 @@ function collectAttemptExplicitToolAllowlistSources(params: { ]); } -function createAttemptAbortError(signal: AbortSignal): Error { - if (signal.reason instanceof Error) { - return signal.reason; - } - const error = new Error(typeof signal.reason === "string" ? signal.reason : "agent run aborted"); - error.name = "AbortError"; - return error; -} - -function throwIfAttemptAbortSignalFired(signal: AbortSignal | undefined): void { - if (signal?.aborted === true) { - throw createAttemptAbortError(signal); - } -} - export async function runEmbeddedAttempt( params: EmbeddedRunAttemptParams, ): Promise { @@ -1298,7 +1281,6 @@ export async function runEmbeddedAttempt( `embedded run start: runId=${params.runId} sessionId=${params.sessionId} provider=${params.provider} model=${params.modelId} thinking=${params.thinkLevel} messageChannel=${params.messageChannel ?? params.messageProvider ?? "unknown"}`, ); const prepStages = createEmbeddedRunStageTracker(); - throwIfAttemptAbortSignalFired(params.abortSignal); const emitPrepStageSummary = (phase: string) => { const summary = prepStages.snapshot(); const shouldWarn = shouldWarnEmbeddedRunStageSummary(summary); @@ -1529,7 +1511,7 @@ export async function runEmbeddedAttempt( assertContextEngineHostSupport({ contextEngine: activeContextEngine, operation: "agent-run", - host: PI_EMBEDDED_CONTEXT_ENGINE_HOST, + host: OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST, }); } const activeContextEnginePluginId = resolveContextEngineOwnerPluginId(activeContextEngine); @@ -1615,7 +1597,7 @@ export async function runEmbeddedAttempt( toolSearchControlsEnabledForRun || codeModeControlsEnabledForRun; let toolSearchCatalogExecutor: ToolSearchCatalogToolExecutor | undefined; - toolSearchCatalogRef = + const toolSearchCatalogRef: ToolSearchCatalogRef | undefined = toolSearchControlsEnabledForRun || codeModeControlsEnabledForRun ? createToolSearchCatalogRef() : undefined; @@ -1867,11 +1849,6 @@ export async function runEmbeddedAttempt( modelApi: params.model.api, model: params.model, }; - const pluginMetadataSnapshot = getCurrentPluginMetadataSnapshot({ - config: params.config, - env: process.env, - workspaceDir: effectiveWorkspace, - }); const tools = normalizeAgentRuntimeTools({ runtimePlan: params.runtimePlan, tools: toolsEnabled ? toolsRaw : [], @@ -1897,7 +1874,7 @@ export async function runEmbeddedAttempt( cfg: params.config, }) : undefined; - bundleMcpRuntime = bundleMcpSessionRuntime + const bundleMcpRuntime = bundleMcpSessionRuntime ? await materializeBundleMcpToolsForRun({ runtime: bundleMcpSessionRuntime, reservedToolNames: [ @@ -1911,7 +1888,7 @@ export async function runEmbeddedAttempt( disableTools: params.disableTools || isRawModelRun, toolsAllow: params.toolsAllow, }); - bundleLspRuntime = bundleLspEnabled + const bundleLspRuntime = bundleLspEnabled ? await createBundleLspToolRuntime({ workspaceDir: effectiveWorkspace, cfg: params.config, @@ -1922,8 +1899,15 @@ export async function runEmbeddedAttempt( ], }) : undefined; + const allowedBundledTools = applyEmbeddedAttemptToolsAllow( + [...(bundleMcpRuntime?.tools ?? []), ...(bundleLspRuntime?.tools ?? [])], + effectiveToolsAllow, + { + toolMeta: (tool) => getPluginToolMeta(tool), + }, + ); const filteredBundledTools = applyFinalEffectiveToolPolicy({ - bundledTools: [...(bundleMcpRuntime?.tools ?? []), ...(bundleLspRuntime?.tools ?? [])], + bundledTools: allowedBundledTools, config: params.config, sandboxToolPolicy: sandbox?.tools, sessionKey: sandboxSessionKey, @@ -1942,9 +1926,6 @@ export async function runEmbeddedAttempt( senderE164: params.senderE164, warn: (message) => log.warn(message), }); - const trustedPluginLocalMediaToolNames = collectTrustedPluginLocalMediaToolNames({ - tools: toolsEnabled ? [...toolsRaw, ...filteredBundledTools] : [], - }); const normalizedBundledTools = filteredBundledTools.length > 0 ? normalizeAgentRuntimeTools({ @@ -2353,9 +2334,6 @@ export async function runEmbeddedAttempt( ...sessionWriteLockOptions, }, }); - releaseRetainedSessionLock = () => sessionLockController.dispose(); - armExternalAbortSignal(); - await throwIfAttemptAbortSignalFiredAfterPrepCleanup(); let sessionManager: ReturnType | undefined; let session: Awaited>["session"] | undefined; @@ -2363,19 +2341,16 @@ export async function runEmbeddedAttempt( let trajectoryRecorder: ReturnType | null = null; let trajectoryEndRecorded = false; let buildAbortSettlePromise: () => Promise | null = () => null; - sessionCleanupOwnsEmbeddedResources = true; try { await repairSessionFileIfNeeded({ sessionFile: params.sessionFile, debug: (message) => log.debug(message), warn: (message) => log.warn(message), }); - await throwIfAttemptAbortSignalFiredAfterPrepCleanup(); const hadSessionFile = await fs .stat(params.sessionFile) .then(() => true) .catch(() => false); - await throwIfAttemptAbortSignalFiredAfterPrepCleanup(); const transcriptPolicy = resolveAttemptTranscriptPolicy({ runtimePlan: params.runtimePlan, @@ -2420,7 +2395,6 @@ export async function runEmbeddedAttempt( }, }); trackSessionManagerAccess(params.sessionFile); - await throwIfAttemptAbortSignalFiredAfterPrepCleanup(); await runAttemptContextEngineBootstrap({ hadSessionFile, @@ -2451,7 +2425,6 @@ export async function runEmbeddedAttempt( }), warn: (message) => log.warn(message), }); - await throwIfAttemptAbortSignalFiredAfterPrepCleanup(); await prepareSessionManagerForRun({ sessionManager, @@ -2460,16 +2433,19 @@ export async function runEmbeddedAttempt( sessionId: params.sessionId, cwd: effectiveWorkspace, }); - await throwIfAttemptAbortSignalFiredAfterPrepCleanup(); - const settingsManager = createPreparedEmbeddedPiSettingsManager({ + const settingsManager = createPreparedEmbeddedAgentSettingsManager({ cwd: effectiveWorkspace, agentDir, cfg: params.config, - pluginMetadataSnapshot, + pluginMetadataSnapshot: getCurrentPluginMetadataSnapshot({ + config: params.config, + env: process.env, + workspaceDir: effectiveWorkspace, + }), contextTokenBudget: params.contextTokenBudget, }); - const piAutoCompactionGuardArgs = { + const autoCompactionGuardArgs = { settingsManager, contextEngineInfo: activeContextEngine?.info, compactionMode: resolveEffectiveCompactionMode(params.config), @@ -2479,36 +2455,34 @@ export async function runEmbeddedAttempt( baseUrl: params.model.baseUrl ?? undefined, }), }; - applyPiAutoCompactionGuard(piAutoCompactionGuardArgs); + applyAgentAutoCompactionGuard(autoCompactionGuardArgs); // Sets compaction/pruning runtime state and returns extension factories // that must be passed to the resource loader for the safeguard to be active. const extensionFactories = buildEmbeddedExtensionFactories({ cfg: params.config, sessionManager, - workspaceDir: effectiveWorkspace, provider: params.provider, modelId: params.modelId, model: params.model, }); - const resourceLoader = createEmbeddedPiResourceLoader({ + const resourceLoader = createEmbeddedAgentResourceLoader({ cwd: resolvedWorkspace, agentDir, settingsManager, extensionFactories, }); await resourceLoader.reload(); - await throwIfAttemptAbortSignalFiredAfterPrepCleanup(); // DefaultResourceLoader.reload() rehydrates settings from disk and can drop OpenClaw - // compaction overrides applied in createPreparedEmbeddedPiSettingsManager — same - // rehydration also restores Pi's auto-compaction (openclaw#75799), so re-apply + // compaction overrides applied in createPreparedEmbeddedAgentSettingsManager — same + // rehydration also restores OpenClaw runtime's auto-compaction (openclaw#75799), so re-apply // both guards. - applyPiCompactionSettingsFromConfig({ + applyAgentCompactionSettingsFromConfig({ settingsManager, cfg: params.config, contextTokenBudget: params.contextTokenBudget, }); - applyPiAutoCompactionGuard(piAutoCompactionGuardArgs); + applyAgentAutoCompactionGuard(autoCompactionGuardArgs); prepStages.mark("session-resource-loader"); // Get hook runner early so it's available when creating tools @@ -2546,8 +2520,10 @@ export async function runEmbeddedAttempt( cfg: params.config, agentId: sessionAgentId, }); - // Exact raw names of every tool registered for this run. This remains - // available for diagnostics; local MEDIA: trust is narrower below. + // Exact raw names of every tool registered for this run, including + // bundled/plugin tools. Used as the raw-name set for the trusted local + // MEDIA: passthrough gate: a normalized alias is not sufficient — the + // emitted tool name must match an exact registration of this run. const builtinToolNames = new Set( uncompactedEffectiveTools.flatMap((tool) => { const name = (tool.name ?? "").trim(); @@ -2563,13 +2539,9 @@ export async function runEmbeddedAttempt( isPluginTool: (tool) => Boolean(getPluginToolMeta(tool as Parameters[0])), }); - const trustedLocalMediaToolNames = collectTrustedLocalMediaToolNames({ - coreBuiltinToolNames, - trustedPluginToolNames: trustedPluginLocalMediaToolNames, - }); const clientToolNameConflicts = findClientToolNameConflicts({ tools: clientTools ?? [], - existingToolNames: [...coreBuiltinToolNames, ...PI_RESERVED_TOOL_NAMES], + existingToolNames: [...coreBuiltinToolNames, ...AGENT_RESERVED_TOOL_NAMES], }); if (clientToolNameConflicts.length > 0) { throw createClientToolNameConflictError(clientToolNameConflicts); @@ -2645,7 +2617,7 @@ export async function runEmbeddedAttempt( } const allCustomTools = [...customTools, ...clientToolDefs]; - // Pi treats `tools` as a name allowlist during session creation. Pass the + // The session runtime treats `tools` as a name allowlist during session creation. Pass the // exact OpenClaw-managed registrations so custom tools survive startup and // client-provided names do not broaden the prompt/runtime boundary. const sessionToolAllowlist = toSessionToolAllowlist( @@ -2672,11 +2644,10 @@ export async function runEmbeddedAttempt( }, }); session = createdSession.session; - await throwIfAttemptAbortSignalFiredAfterPrepCleanup(); + applySystemPromptOverrideToSession(session, systemPromptText); if (!session) { throw new Error("Embedded agent session missing"); } - applySystemPromptOverrideToSession(session, systemPromptText); session.setActiveToolsByName(sessionToolAllowlist); const activeSession = session; installSessionEventWriteLock({ @@ -2695,7 +2666,7 @@ export async function runEmbeddedAttempt( if (isRawModelRun) { // Raw model probes should measure exactly the requested prompt against // the selected provider/model. Reset clears restored transcript state - // and queues; the empty system override prevents Pi from rebuilding the + // and queues; the empty system override prevents the runtime from rebuilding the // normal OpenClaw agent/tool prompt when `session.prompt()` starts. activeSession.agent.reset(); applySystemPromptOverrideToSession(activeSession, ""); @@ -2734,10 +2705,6 @@ export async function runEmbeddedAttempt( trackSettlePromise(inFlightAbortSettlePromises, promise); const abortActiveSession = (): Promise => trackAbortSettlePromise(Promise.resolve(activeSession.abort())); - abortActiveSessionForExternalSignal = abortActiveSession; - if (runAbortController.signal.aborted) { - void abortActiveSession(); - } buildAbortSettlePromise = (): Promise | null => { const promises = [...inFlightPromptSettlePromises, ...inFlightAbortSettlePromises]; if (promises.length === 0) { @@ -2745,7 +2712,6 @@ export async function runEmbeddedAttempt( } return Promise.allSettled(promises).then(() => undefined); }; - let heartbeatResponseTerminated = false; abortSessionForYield = () => { yieldAbortSettled = abortActiveSession(); }; @@ -3176,7 +3142,7 @@ export async function runEmbeddedAttempt( }; // Some models emit tool names with surrounding whitespace (e.g. " read "). - // pi-agent-core dispatches tool calls with exact string matching, so normalize + // agent runtime dispatches tool calls with exact string matching, so normalize // names on the live response stream before tool execution. activeSession.agent.streamFn = wrapStreamFnSanitizeMalformedToolCalls( activeSession.agent.streamFn, @@ -3214,7 +3180,7 @@ export async function runEmbeddedAttempt( activeSession.agent.streamFn, ); } - // Anthropic-compatible providers can add new stop reasons before pi-ai maps them. + // Anthropic-compatible providers can add new stop reasons before shared model runtime maps them. // Recover the known "sensitive" stop reason here so a model refusal does not // bubble out as an uncaught runner error and stall channel polling. activeSession.agent.streamFn = wrapStreamFnHandleSensitiveStopReason( @@ -3460,6 +3426,13 @@ export async function runEmbeddedAttempt( } let yieldAborted = false; + const getAbortReason = (signal: AbortSignal): unknown => + "reason" in signal ? (signal as { reason?: unknown }).reason : undefined; + const makeTimeoutAbortReason = (): Error => { + const err = new Error("request timed out"); + err.name = "TimeoutError"; + return err; + }; const abortCompaction = () => { if (!activeSession.isCompacting) { return; @@ -3490,7 +3463,6 @@ export async function runEmbeddedAttempt( abortCompaction(); void abortActiveSession(); }; - abortRunForExternalSignal = abortRun; idleTimeoutTrigger = (error) => { idleTimedOut = true; abortRun(true, error); @@ -3509,12 +3481,9 @@ export async function runEmbeddedAttempt( prompt: string, options?: Parameters[1], ): Promise => - withOwnedSessionTranscriptWrites(ownedTranscriptWriteContext, async () => { - if (runAbortController.signal.aborted) { - throw createAttemptAbortError(runAbortController.signal); - } - return await abortable(trackPromptSettlePromise(activeSession.prompt(prompt, options))); - }); + withOwnedSessionTranscriptWrites(ownedTranscriptWriteContext, async () => + abortable(trackPromptSettlePromise(activeSession.prompt(prompt, options))), + ); const onBlockReply = params.onBlockReply ? bindOwnedSessionTranscriptWrites(ownedTranscriptWriteContext, params.onBlockReply) : undefined; @@ -3522,7 +3491,7 @@ export async function runEmbeddedAttempt( ? bindOwnedSessionTranscriptWrites(ownedTranscriptWriteContext, params.onBlockReplyFlush) : undefined; - const subscription = subscribeEmbeddedPiSession( + const subscription = subscribeEmbeddedAgentSession( buildEmbeddedSubscriptionParams({ session: activeSession, runId: params.runId, @@ -3546,16 +3515,6 @@ export async function runEmbeddedAttempt( onAssistantMessageStart: params.onAssistantMessageStart, onExecutionPhase: params.onExecutionPhase, onAgentEvent: params.onAgentEvent, - onHeartbeatToolResponse: - params.trigger === "heartbeat" - ? () => { - if (heartbeatResponseTerminated) { - return; - } - heartbeatResponseTerminated = true; - void abortActiveSession(); - } - : undefined, terminalLifecyclePhase: params.deferTerminalLifecycleEnd ? "finishing" : "end", onBeforeLifecycleTerminal: () => { // Clear embedded-run activity before emitting terminal lifecycle events so @@ -3574,7 +3533,6 @@ export async function runEmbeddedAttempt( sessionId: params.sessionId, agentId: sessionAgentId, builtinToolNames, - trustedLocalMediaToolNames, internalEvents: params.internalEvents, }), ); @@ -3605,8 +3563,6 @@ export async function runEmbeddedAttempt( getCompactionCount, getLastCompactionTokensAfter, } = subscription; - isCompactionPendingForExternalSignal = () => subscription.isCompacting(); - isCompactionInFlightForExternalSignal = () => activeSession.isCompacting; toolSearchCatalogExecutor = async (toolParams) => { try { const result = await runToolLifecycle({ @@ -3649,7 +3605,7 @@ export async function runEmbeddedAttempt( } }; - const queueHandle: EmbeddedPiQueueHandle & { + const queueHandle: EmbeddedAgentQueueHandle & { kind: "embedded"; cancel: (reason?: "user_abort" | "restart" | "superseded") => void; } = { @@ -3746,9 +3702,29 @@ export async function runEmbeddedAttempt( let messagesSnapshot: AgentMessage[] = []; let sessionIdUsed = activeSession.sessionId; let sessionFileUsed: string | undefined = params.sessionFile; - if (params.abortSignal?.aborted === true) { - onExternalAbortSignal(); - await throwIfAttemptAbortSignalFiredAfterPrepCleanup(); + const onAbort = () => { + externalAbort = true; + const reason = params.abortSignal ? getAbortReason(params.abortSignal) : undefined; + const timeout = reason ? isTimeoutError(reason) : false; + if ( + shouldFlagCompactionTimeout({ + isTimeout: timeout, + isCompactionPendingOrRetrying: subscription.isCompacting(), + isCompactionInFlight: activeSession.isCompacting, + }) + ) { + timedOutDuringCompaction = true; + } + abortRun(timeout, reason); + }; + if (params.abortSignal) { + if (params.abortSignal.aborted) { + onAbort(); + } else { + params.abortSignal.addEventListener("abort", onAbort, { + once: true, + }); + } } // Hook runner was already obtained earlier before tool creation @@ -4176,7 +4152,7 @@ export async function runEmbeddedAttempt( } // Detect and load images referenced in the visible prompt for vision-capable models. - // Images are prompt-local only (pi-like behavior). + // Images are prompt-local only. const imageResult = skipPromptSubmission ? { images: [], @@ -4508,9 +4484,6 @@ export async function runEmbeddedAttempt( await persistSessionsYieldContextMessage(activeSession, yieldMessage); } }); - } else if (heartbeatResponseTerminated && isRunnerAbortError(err)) { - aborted = false; - await sessionLockController.waitForSessionEvents(activeSession); } else if (isMidTurnPrecheckSignal(err)) { await sessionLockController.waitForSessionEvents(activeSession); await sessionLockController.withSessionWriteLock(() => { @@ -4584,11 +4557,6 @@ export async function runEmbeddedAttempt( `proceeding with pre-compaction state runId=${params.runId} sessionId=${params.sessionId}`, ); } - } else if (onBlockReplyFlush) { - // Retry-generated blocks can still be draining when the compaction - // retry wait resolves; this second drain is idempotent when no new - // blocks were produced. - await onBlockReplyFlush(); } } catch (err) { if (isRunnerAbortError(err)) { @@ -4884,23 +4852,15 @@ export async function runEmbeddedAttempt( params.sessionKey, params.sessionFile, ); + params.abortSignal?.removeEventListener?.("abort", onAbort); } const toolMetasNormalized = toolMetas .filter( - (entry): entry is { toolName: string; meta?: string; asyncStarted?: boolean } => + (entry): entry is { toolName: string; meta?: string } => typeof entry.toolName === "string" && entry.toolName.trim().length > 0, ) - .map((entry) => { - const normalized: { toolName: string; meta?: string; asyncStarted?: boolean } = { - toolName: entry.toolName, - meta: entry.meta, - }; - if (entry.asyncStarted === true) { - normalized.asyncStarted = true; - } - return normalized; - }); + .map((entry) => ({ toolName: entry.toolName, meta: entry.meta })); if (cacheObservabilityEnabled) { const cacheBreakForLog = cacheBreak as PromptCacheBreak | null; if (cacheBreakForLog) { @@ -5072,7 +5032,6 @@ export async function runEmbeddedAttempt( replayMetadata, promptErrorSource, timedOutDuringCompaction, - toolMetas: toolMetasNormalized, }, }); const terminalAssistantTexts = resolveTerminalAssistantTexts({ @@ -5228,7 +5187,7 @@ export async function runEmbeddedAttempt( await runAgentCleanupStep({ runId: params.runId, sessionId: params.sessionId, - step: "pi-trajectory-flush", + step: "openclaw-trajectory-flush", log, getTimeoutDetails: () => trajectoryRecorder?.describeFlushState(), cleanup: async () => { @@ -5238,7 +5197,7 @@ export async function runEmbeddedAttempt( // Always tear down the session (and release the lock) before we leave this attempt. // // BUGFIX: Wait for the agent to be truly idle before flushing pending tool results. - // pi-agent-core's auto-retry resolves waitForRetry() on assistant message receipt, + // agent runtime's auto-retry resolves waitForRetry() on assistant message receipt, // *before* tool execution completes in the retried agent loop. Without this wait, // flushPendingToolResults() fires while tools are still executing, inserting // synthetic "missing tool result" errors and causing silent agent failures. diff --git a/src/agents/pi-embedded-runner/run/auth-controller.test.ts b/src/agents/embedded-agent-runner/run/auth-controller.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/auth-controller.test.ts rename to src/agents/embedded-agent-runner/run/auth-controller.test.ts index a9b80e387b4..fd55641950b 100644 --- a/src/agents/pi-embedded-runner/run/auth-controller.test.ts +++ b/src/agents/embedded-agent-runner/run/auth-controller.test.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { beforeEach, describe, expect, it, vi, type Mock } from "vitest"; import type { AuthProfileStore } from "../../auth-profiles.js"; import type { RuntimeAuthState } from "./helpers.js"; @@ -41,7 +41,7 @@ function createDeferred() { return { promise, resolve, reject }; } -function createTestModel(): Model { +function createTestModel(): Model { return { id: "test-model", name: "test-model", @@ -56,7 +56,7 @@ function createTestModel(): Model { cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 8_000, maxTokens: 4_000, - } as Model; + } as Model; } function getRuntimeAuthSnapshot( @@ -66,8 +66,8 @@ function getRuntimeAuthSnapshot( } type MutableAuthControllerHarness = { - runtimeModel: Model; - effectiveModel: Model; + runtimeModel: Model; + effectiveModel: Model; apiKeyInfo: unknown; lastProfileId?: string; runtimeAuthState: RuntimeAuthState | null; diff --git a/src/agents/pi-embedded-runner/run/auth-controller.ts b/src/agents/embedded-agent-runner/run/auth-controller.ts similarity index 97% rename from src/agents/pi-embedded-runner/run/auth-controller.ts rename to src/agents/embedded-agent-runner/run/auth-controller.ts index 9f3439db84a..714c30caf9f 100644 --- a/src/agents/pi-embedded-runner/run/auth-controller.ts +++ b/src/agents/embedded-agent-runner/run/auth-controller.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import type { ThinkLevel } from "../../../auto-reply/thinking.js"; import { formatErrorMessage } from "../../../infra/errors.js"; import { prepareProviderRuntimeAuth } from "../../../plugins/provider-runtime.js"; @@ -7,6 +7,11 @@ import { isProfileInCooldown, resolveProfilesUnavailableReason, } from "../../auth-profiles.js"; +import { + classifyFailoverReason, + isFailoverErrorMessage, + type FailoverReason, +} from "../../embedded-agent-helpers.js"; import { FailoverError, resolveFailoverStatus } from "../../failover-error.js"; import { shouldAllowCooldownProbeForReason } from "../../failover-policy.js"; import { @@ -14,11 +19,6 @@ import { getApiKeyForModel, type ResolvedProviderAuth, } from "../../model-auth.js"; -import { - classifyFailoverReason, - isFailoverErrorMessage, - type FailoverReason, -} from "../../pi-embedded-helpers.js"; import { resolveProviderRequestConfig, sanitizeRuntimeProviderRequestOverrides, @@ -30,7 +30,7 @@ import { RUNTIME_AUTH_REFRESH_RETRY_MS, type RuntimeAuthState, } from "./helpers.js"; -import type { RunEmbeddedPiAgentParams } from "./params.js"; +import type { RunEmbeddedAgentParams } from "./params.js"; type ApiKeyInfo = ResolvedProviderAuth; @@ -45,7 +45,7 @@ type LogLike = { }; export function createEmbeddedRunAuthController(params: { - config: RunEmbeddedPiAgentParams["config"]; + config: RunEmbeddedAgentParams["config"]; agentDir: string; workspaceDir: string; authStore: AuthProfileStore; @@ -58,10 +58,10 @@ export function createEmbeddedRunAuthController(params: { allowTransientCooldownProbe: boolean; getProvider(): string; getModelId(): string; - getRuntimeModel(): Model; - setRuntimeModel(next: Model): void; - getEffectiveModel(): Model; - setEffectiveModel(next: Model): void; + getRuntimeModel(): Model; + setRuntimeModel(next: Model): void; + getEffectiveModel(): Model; + setEffectiveModel(next: Model): void; getApiKeyInfo(): ApiKeyInfo | null; setApiKeyInfo(next: ApiKeyInfo | null): void; getLastProfileId(): string | undefined; @@ -76,7 +76,7 @@ export function createEmbeddedRunAuthController(params: { log: LogLike; }) { const applyPreparedRuntimeRequestOverrides = (paramsForApply: { - runtimeModel: Model; + runtimeModel: Model; preparedAuth: { baseUrl?: string; request?: Parameters[0]["request"]; @@ -120,7 +120,7 @@ export function createEmbeddedRunAuthController(params: { const nextRuntimeAuthGeneration = () => (params.getRuntimeAuthState()?.generation ?? 0) + 1; const prepareRuntimeAuthForModel = async (prepareParams: { - runtimeModel: Model; + runtimeModel: Model; apiKey: string; authMode: string; profileId?: string; @@ -374,7 +374,7 @@ export function createEmbeddedRunAuthController(params: { // AWS SDK auth via IMDS / instance role / ECS task role: no explicit API // key is available but the SDK default credential chain can resolve // credentials at runtime. We must still call setRuntimeApiKey so that - // pi's authStorage considers the provider authenticated. Try + // OpenClaw runtime's authStorage considers the provider authenticated. Try // prepareProviderRuntimeAuth first (it can sign requests and return a // short-lived token); fall back to a sentinel value when the provider // plugin does not implement runtime auth preparation. @@ -410,7 +410,7 @@ export function createEmbeddedRunAuthController(params: { ); } // No runtime auth plugin resolved a real credential. Inject the - // sentinel so pi's hasConfiguredAuth() passes and the AWS SDK default + // sentinel so OpenClaw runtime's hasConfiguredAuth() passes and the AWS SDK default // credential chain handles actual request signing. clearRuntimeAuthRefreshTimer(); params.authStorage.setRuntimeApiKey(runtimeModel.provider, AWS_SDK_AUTH_SENTINEL); diff --git a/src/agents/pi-embedded-runner/run/auth-profile-failure-policy.test.ts b/src/agents/embedded-agent-runner/run/auth-profile-failure-policy.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/auth-profile-failure-policy.test.ts rename to src/agents/embedded-agent-runner/run/auth-profile-failure-policy.test.ts diff --git a/src/agents/pi-embedded-runner/run/auth-profile-failure-policy.ts b/src/agents/embedded-agent-runner/run/auth-profile-failure-policy.ts similarity index 94% rename from src/agents/pi-embedded-runner/run/auth-profile-failure-policy.ts rename to src/agents/embedded-agent-runner/run/auth-profile-failure-policy.ts index b0af3f82fa5..d01fed1a5fc 100644 --- a/src/agents/pi-embedded-runner/run/auth-profile-failure-policy.ts +++ b/src/agents/embedded-agent-runner/run/auth-profile-failure-policy.ts @@ -1,5 +1,5 @@ import type { AuthProfileFailureReason } from "../../auth-profiles/types.js"; -import type { FailoverReason } from "../../pi-embedded-helpers/types.js"; +import type { FailoverReason } from "../../embedded-agent-helpers/types.js"; import type { AuthProfileFailurePolicy } from "./auth-profile-failure-policy.types.js"; export function resolveAuthProfileFailureReason(params: { diff --git a/src/agents/pi-embedded-runner/run/auth-profile-failure-policy.types.ts b/src/agents/embedded-agent-runner/run/auth-profile-failure-policy.types.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/auth-profile-failure-policy.types.ts rename to src/agents/embedded-agent-runner/run/auth-profile-failure-policy.types.ts diff --git a/src/agents/pi-embedded-runner/run/backend.test.ts b/src/agents/embedded-agent-runner/run/backend.test.ts similarity index 74% rename from src/agents/pi-embedded-runner/run/backend.test.ts rename to src/agents/embedded-agent-runner/run/backend.test.ts index 415caa5f23a..679c8492ca3 100644 --- a/src/agents/pi-embedded-runner/run/backend.test.ts +++ b/src/agents/embedded-agent-runner/run/backend.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it } from "vitest"; -import { resolveEmbeddedAgentRuntime } from "../runtime.js"; +import { resolveEmbeddedAgentRuntime } from "../../agent-runtime-id.js"; describe("resolveEmbeddedAgentRuntime", () => { - it("uses PI mode by default", () => { - expect(resolveEmbeddedAgentRuntime({})).toBe("pi"); + it("uses OpenClaw mode by default", () => { + expect(resolveEmbeddedAgentRuntime({})).toBe("openclaw"); }); - it("accepts the PI kill switch", () => { - expect(resolveEmbeddedAgentRuntime({ OPENCLAW_AGENT_RUNTIME: "pi" })).toBe("pi"); + it("accepts the OpenClaw runtime override", () => { + expect(resolveEmbeddedAgentRuntime({ OPENCLAW_AGENT_RUNTIME: "openclaw" })).toBe("openclaw"); }); it("canonicalizes legacy Codex app-server runtime ids", () => { diff --git a/src/agents/pi-embedded-runner/run/backend.ts b/src/agents/embedded-agent-runner/run/backend.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/backend.ts rename to src/agents/embedded-agent-runner/run/backend.ts diff --git a/src/agents/pi-embedded-runner/run/codex-app-server-recovery.ts b/src/agents/embedded-agent-runner/run/codex-app-server-recovery.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/codex-app-server-recovery.ts rename to src/agents/embedded-agent-runner/run/codex-app-server-recovery.ts diff --git a/src/agents/pi-embedded-runner/run/compaction-retry-aggregate-timeout.test.ts b/src/agents/embedded-agent-runner/run/compaction-retry-aggregate-timeout.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/compaction-retry-aggregate-timeout.test.ts rename to src/agents/embedded-agent-runner/run/compaction-retry-aggregate-timeout.test.ts diff --git a/src/agents/pi-embedded-runner/run/compaction-retry-aggregate-timeout.ts b/src/agents/embedded-agent-runner/run/compaction-retry-aggregate-timeout.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/compaction-retry-aggregate-timeout.ts rename to src/agents/embedded-agent-runner/run/compaction-retry-aggregate-timeout.ts diff --git a/src/agents/pi-embedded-runner/run/compaction-timeout.test.ts b/src/agents/embedded-agent-runner/run/compaction-timeout.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/compaction-timeout.test.ts rename to src/agents/embedded-agent-runner/run/compaction-timeout.test.ts diff --git a/src/agents/pi-embedded-runner/run/compaction-timeout.ts b/src/agents/embedded-agent-runner/run/compaction-timeout.ts similarity index 96% rename from src/agents/pi-embedded-runner/run/compaction-timeout.ts rename to src/agents/embedded-agent-runner/run/compaction-timeout.ts index 4f8691e8fd0..f4cb930403b 100644 --- a/src/agents/pi-embedded-runner/run/compaction-timeout.ts +++ b/src/agents/embedded-agent-runner/run/compaction-timeout.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../../runtime/index.js"; export type CompactionTimeoutSignal = { isTimeout: boolean; diff --git a/src/agents/pi-embedded-runner/run/failover-observation.test.ts b/src/agents/embedded-agent-runner/run/failover-observation.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/failover-observation.test.ts rename to src/agents/embedded-agent-runner/run/failover-observation.test.ts diff --git a/src/agents/pi-embedded-runner/run/failover-observation.ts b/src/agents/embedded-agent-runner/run/failover-observation.ts similarity index 96% rename from src/agents/pi-embedded-runner/run/failover-observation.ts rename to src/agents/embedded-agent-runner/run/failover-observation.ts index 0de896a47ce..b5c3cbbeaf7 100644 --- a/src/agents/pi-embedded-runner/run/failover-observation.ts +++ b/src/agents/embedded-agent-runner/run/failover-observation.ts @@ -4,8 +4,8 @@ import { buildApiErrorObservationFields, sanitizeForConsole, shouldSuppressRawErrorConsoleSuffix, -} from "../../pi-embedded-error-observation.js"; -import type { FailoverReason } from "../../pi-embedded-helpers.js"; +} from "../../embedded-agent-error-observation.js"; +import type { FailoverReason } from "../../embedded-agent-helpers.js"; import { log } from "../logger.js"; export type FailoverDecisionLoggerInput = { diff --git a/src/agents/pi-embedded-runner/run/failover-policy.test.ts b/src/agents/embedded-agent-runner/run/failover-policy.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/failover-policy.test.ts rename to src/agents/embedded-agent-runner/run/failover-policy.test.ts diff --git a/src/agents/pi-embedded-runner/run/failover-policy.ts b/src/agents/embedded-agent-runner/run/failover-policy.ts similarity index 98% rename from src/agents/pi-embedded-runner/run/failover-policy.ts rename to src/agents/embedded-agent-runner/run/failover-policy.ts index 2159947f117..e96db12e8c0 100644 --- a/src/agents/pi-embedded-runner/run/failover-policy.ts +++ b/src/agents/embedded-agent-runner/run/failover-policy.ts @@ -1,4 +1,4 @@ -import type { FailoverReason } from "../../pi-embedded-helpers.js"; +import type { FailoverReason } from "../../embedded-agent-helpers.js"; export type RunFailoverDecision = | { diff --git a/src/agents/pi-embedded-runner/run/fallbacks.test.ts b/src/agents/embedded-agent-runner/run/fallbacks.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/fallbacks.test.ts rename to src/agents/embedded-agent-runner/run/fallbacks.test.ts diff --git a/src/agents/pi-embedded-runner/run/fallbacks.ts b/src/agents/embedded-agent-runner/run/fallbacks.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/fallbacks.ts rename to src/agents/embedded-agent-runner/run/fallbacks.ts diff --git a/src/agents/pi-embedded-runner/run/helpers.resolve-error-context.test.ts b/src/agents/embedded-agent-runner/run/helpers.resolve-error-context.test.ts similarity index 86% rename from src/agents/pi-embedded-runner/run/helpers.resolve-error-context.test.ts rename to src/agents/embedded-agent-runner/run/helpers.resolve-error-context.test.ts index 84792e7fbed..2a3442b3d56 100644 --- a/src/agents/pi-embedded-runner/run/helpers.resolve-error-context.test.ts +++ b/src/agents/embedded-agent-runner/run/helpers.resolve-error-context.test.ts @@ -23,13 +23,13 @@ describe("resolveActiveErrorContext", () => { expect(result).toEqual({ provider: "openai", model: "gpt-5.4-codex" }); }); - it("ignores the embedded PI harness provider when the model provider is known", () => { + it("ignores the embedded OpenClaw harness provider when the model provider is known", () => { const result = resolveActiveErrorContext({ provider: "openrouter", model: "openai/gpt-5.4", assistant: { - provider: "pi", - model: "pi", + provider: "openclaw", + model: "openclaw", }, }); diff --git a/src/agents/pi-embedded-runner/run/helpers.test.ts b/src/agents/embedded-agent-runner/run/helpers.test.ts similarity index 97% rename from src/agents/pi-embedded-runner/run/helpers.test.ts rename to src/agents/embedded-agent-runner/run/helpers.test.ts index 3ff1369455b..a8aaae9411c 100644 --- a/src/agents/pi-embedded-runner/run/helpers.test.ts +++ b/src/agents/embedded-agent-runner/run/helpers.test.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { createUsageAccumulator } from "../usage-accumulator.js"; import { diff --git a/src/agents/pi-embedded-runner/run/helpers.ts b/src/agents/embedded-agent-runner/run/helpers.ts similarity index 95% rename from src/agents/pi-embedded-runner/run/helpers.ts rename to src/agents/embedded-agent-runner/run/helpers.ts index 684ac0244b5..aa4af8e1c23 100644 --- a/src/agents/pi-embedded-runner/run/helpers.ts +++ b/src/agents/embedded-agent-runner/run/helpers.ts @@ -1,11 +1,11 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { generateSecureToken } from "../../../infra/secure-random.js"; import { extractAssistantTextForPhase } from "../../../shared/chat-message-content.js"; import { resolveAgentConfig } from "../../agent-scope-config.js"; -import { extractAssistantVisibleText } from "../../pi-embedded-utils.js"; +import { extractAssistantVisibleText } from "../../embedded-agent-utils.js"; import { derivePromptTokens, normalizeUsage } from "../../usage.js"; -import type { EmbeddedPiAgentMeta } from "../types.js"; +import type { EmbeddedAgentMeta } from "../types.js"; import { toLastCallUsage, toNormalizedUsage, type UsageAccumulator } from "../usage-accumulator.js"; type UsageSnapshot = { @@ -116,7 +116,7 @@ export function isAssistantForModelRef( } function isEmbeddedHarnessProvider(provider: string): boolean { - return provider.trim().toLowerCase() === "pi"; + return provider.trim().toLowerCase() === "openclaw"; } export function resolveReportedModelRef(params: { @@ -152,7 +152,7 @@ export function buildUsageAgentMetaFields(params: { lastAssistantUsage?: UsageSnapshot | null; lastRunPromptUsage: UsageSnapshot | undefined; lastTurnTotal?: number; -}): Pick { +}): Pick { const usage = toNormalizedUsage(params.usageAccumulator); if (usage && params.lastTurnTotal && params.lastTurnTotal > 0) { usage.total = params.lastTurnTotal; @@ -183,7 +183,7 @@ export function buildErrorAgentMeta(params: { lastRunPromptUsage: UsageSnapshot | undefined; lastAssistant?: { usage?: unknown } | null; lastTurnTotal?: number; -}): EmbeddedPiAgentMeta { +}): EmbeddedAgentMeta { const usageMeta = buildUsageAgentMetaFields({ usageAccumulator: params.usageAccumulator, lastAssistantUsage: params.lastAssistant?.usage as UsageSnapshot | undefined, diff --git a/src/agents/pi-embedded-runner/run/history-image-prune.test.ts b/src/agents/embedded-agent-runner/run/history-image-prune.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/history-image-prune.test.ts rename to src/agents/embedded-agent-runner/run/history-image-prune.test.ts index 86cce22fb7c..53075c895ab 100644 --- a/src/agents/pi-embedded-runner/run/history-image-prune.test.ts +++ b/src/agents/embedded-agent-runner/run/history-image-prune.test.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { ImageContent } from "@earendil-works/pi-ai"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import type { ImageContent } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { castAgentMessage } from "../../test-helpers/agent-message-fixtures.js"; import { diff --git a/src/agents/pi-embedded-runner/run/history-image-prune.ts b/src/agents/embedded-agent-runner/run/history-image-prune.ts similarity index 98% rename from src/agents/pi-embedded-runner/run/history-image-prune.ts rename to src/agents/embedded-agent-runner/run/history-image-prune.ts index bdeefd8d1a7..3561be9abb0 100644 --- a/src/agents/pi-embedded-runner/run/history-image-prune.ts +++ b/src/agents/embedded-agent-runner/run/history-image-prune.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../../runtime/index.js"; export const PRUNED_HISTORY_IMAGE_MARKER = "[image data removed - already processed by model]"; export const PRUNED_HISTORY_MEDIA_REFERENCE_MARKER = diff --git a/src/agents/pi-embedded-runner/run/idle-timeout-breaker.test.ts b/src/agents/embedded-agent-runner/run/idle-timeout-breaker.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/idle-timeout-breaker.test.ts rename to src/agents/embedded-agent-runner/run/idle-timeout-breaker.test.ts diff --git a/src/agents/pi-embedded-runner/run/idle-timeout-breaker.ts b/src/agents/embedded-agent-runner/run/idle-timeout-breaker.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/idle-timeout-breaker.ts rename to src/agents/embedded-agent-runner/run/idle-timeout-breaker.ts diff --git a/src/agents/pi-embedded-runner/run/images.test.ts b/src/agents/embedded-agent-runner/run/images.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/images.test.ts rename to src/agents/embedded-agent-runner/run/images.test.ts diff --git a/src/agents/pi-embedded-runner/run/images.ts b/src/agents/embedded-agent-runner/run/images.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/images.ts rename to src/agents/embedded-agent-runner/run/images.ts index 5113e6b1873..d369511d51d 100644 --- a/src/agents/pi-embedded-runner/run/images.ts +++ b/src/agents/embedded-agent-runner/run/images.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import type { ImageContent } from "@earendil-works/pi-ai"; +import type { ImageContent } from "openclaw/plugin-sdk/llm"; import { formatErrorMessage } from "../../../infra/errors.js"; import { assertNoWindowsNetworkPath, safeFileURLToPath } from "../../../infra/local-file-access.js"; import { resolveMediaReferenceLocalPath } from "../../../media/media-reference.js"; diff --git a/src/agents/pi-embedded-runner/run/incomplete-turn.ts b/src/agents/embedded-agent-runner/run/incomplete-turn.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/incomplete-turn.ts rename to src/agents/embedded-agent-runner/run/incomplete-turn.ts index 5ca10a6b570..bc0963d634d 100644 --- a/src/agents/pi-embedded-runner/run/incomplete-turn.ts +++ b/src/agents/embedded-agent-runner/run/incomplete-turn.ts @@ -1,10 +1,9 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { isSilentReplyPayloadText, isSilentReplyText, SILENT_REPLY_TOKEN, } from "../../../auto-reply/tokens.js"; -import type { EmbeddedPiExecutionContract } from "../../../config/types.agent-defaults.js"; +import type { EmbeddedAgentExecutionContract } from "../../../config/types.agent-defaults.js"; import { normalizeLowercaseStringOrEmpty } from "../../../shared/string-coerce.js"; import { normalizeStringEntries } from "../../../shared/string-normalization.js"; import { hasAcceptedSessionSpawn } from "../../accepted-session-spawn.js"; @@ -13,6 +12,7 @@ import { isStrictAgenticSupportedProviderModel, stripProviderPrefix, } from "../../execution-contract.js"; +import type { AgentMessage } from "../../runtime/index.js"; import { isLikelyMutatingToolName } from "../../tool-mutation.js"; import { hasCommittedMessagingToolDeliveryEvidence, @@ -822,7 +822,7 @@ function isSingleActionThenNarrativePattern(params: { } export function resolvePlanningOnlyRetryLimit( - executionContract?: EmbeddedPiExecutionContract, + executionContract?: EmbeddedAgentExecutionContract, ): number { return executionContract === "strict-agentic" ? STRICT_AGENTIC_PLANNING_ONLY_RETRY_LIMIT diff --git a/src/agents/pi-embedded-runner/run/llm-idle-timeout.test.ts b/src/agents/embedded-agent-runner/run/llm-idle-timeout.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/llm-idle-timeout.test.ts rename to src/agents/embedded-agent-runner/run/llm-idle-timeout.test.ts index ff797f1c473..3c69dd15e9d 100644 --- a/src/agents/pi-embedded-runner/run/llm-idle-timeout.test.ts +++ b/src/agents/embedded-agent-runner/run/llm-idle-timeout.test.ts @@ -1,4 +1,4 @@ -import type { AssistantMessageEventStream } from "@earendil-works/pi-ai"; +import type { AssistantMessageEventStream } from "openclaw/plugin-sdk/llm"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; import { diff --git a/src/agents/pi-embedded-runner/run/llm-idle-timeout.ts b/src/agents/embedded-agent-runner/run/llm-idle-timeout.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/llm-idle-timeout.ts rename to src/agents/embedded-agent-runner/run/llm-idle-timeout.ts index f37287e1e69..d22317023c4 100644 --- a/src/agents/pi-embedded-runner/run/llm-idle-timeout.ts +++ b/src/agents/embedded-agent-runner/run/llm-idle-timeout.ts @@ -1,7 +1,7 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { streamSimple } from "@earendil-works/pi-ai"; +import { streamSimple } from "openclaw/plugin-sdk/llm"; import { DEFAULT_LLM_IDLE_TIMEOUT_SECONDS } from "../../../config/agent-timeout-defaults.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; +import type { StreamFn } from "../../runtime/index.js"; import { createStreamIteratorWrapper } from "../../stream-iterator-wrapper.js"; import type { EmbeddedRunTrigger } from "./params.js"; diff --git a/src/agents/pi-embedded-runner/run/message-merge-strategy.test.ts b/src/agents/embedded-agent-runner/run/message-merge-strategy.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/message-merge-strategy.test.ts rename to src/agents/embedded-agent-runner/run/message-merge-strategy.test.ts diff --git a/src/agents/pi-embedded-runner/run/message-merge-strategy.ts b/src/agents/embedded-agent-runner/run/message-merge-strategy.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/message-merge-strategy.ts rename to src/agents/embedded-agent-runner/run/message-merge-strategy.ts diff --git a/src/agents/pi-embedded-runner/run/message-tool-terminal.test.ts b/src/agents/embedded-agent-runner/run/message-tool-terminal.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/message-tool-terminal.test.ts rename to src/agents/embedded-agent-runner/run/message-tool-terminal.test.ts index 510e5f00ec1..91759a02dde 100644 --- a/src/agents/pi-embedded-runner/run/message-tool-terminal.test.ts +++ b/src/agents/embedded-agent-runner/run/message-tool-terminal.test.ts @@ -1,4 +1,4 @@ -import type { Agent, AfterToolCallContext } from "@earendil-works/pi-agent-core"; +import type { Agent, AfterToolCallContext } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it, vi } from "vitest"; import { installMessageToolOnlyTerminalHook, diff --git a/src/agents/pi-embedded-runner/run/message-tool-terminal.ts b/src/agents/embedded-agent-runner/run/message-tool-terminal.ts similarity index 93% rename from src/agents/pi-embedded-runner/run/message-tool-terminal.ts rename to src/agents/embedded-agent-runner/run/message-tool-terminal.ts index 99faaa66ddb..0aeb2c0a3ec 100644 --- a/src/agents/pi-embedded-runner/run/message-tool-terminal.ts +++ b/src/agents/embedded-agent-runner/run/message-tool-terminal.ts @@ -1,12 +1,7 @@ -import type { - AfterToolCallContext, - AfterToolCallResult, - Agent, -} from "@earendil-works/pi-agent-core"; import type { SourceReplyDeliveryMode } from "../../../auto-reply/get-reply-options.types.js"; -import { hasNonEmptyString as hasStringValue } from "../../../shared/string-coerce.js"; -import { isMessageToolSendActionName } from "../../pi-embedded-messaging.js"; -import { isToolResultError } from "../../pi-embedded-subscribe.tools.js"; +import { isMessageToolSendActionName } from "../../embedded-agent-messaging.js"; +import { isToolResultError } from "../../embedded-agent-subscribe.tools.js"; +import type { AfterToolCallContext, AfterToolCallResult, Agent } from "../../runtime/index.js"; import { normalizeToolName } from "../../tool-policy.js"; const MESSAGE_TOOL_NAME = "message"; @@ -28,10 +23,14 @@ function argsRecordForToolCall(context: AfterToolCallContext): Record) + ? fallbackArgs : {}; } +function hasStringValue(value: unknown): boolean { + return typeof value === "string" && value.trim().length > 0; +} + function hasExplicitMessageRoute(args: Record): boolean { if (EXPLICIT_MESSAGE_ROUTE_KEYS.some((key) => hasStringValue(args[key]))) { return true; diff --git a/src/agents/pi-embedded-runner/run/midturn-precheck.ts b/src/agents/embedded-agent-runner/run/midturn-precheck.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/midturn-precheck.ts rename to src/agents/embedded-agent-runner/run/midturn-precheck.ts diff --git a/src/agents/pi-embedded-runner/run/params.ts b/src/agents/embedded-agent-runner/run/params.ts similarity index 97% rename from src/agents/pi-embedded-runner/run/params.ts rename to src/agents/embedded-agent-runner/run/params.ts index 8c7c9ae31ed..e8eec279d36 100644 --- a/src/agents/pi-embedded-runner/run/params.ts +++ b/src/agents/embedded-agent-runner/run/params.ts @@ -1,5 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { ImageContent } from "@earendil-works/pi-ai"; +import type { ImageContent } from "openclaw/plugin-sdk/llm"; import type { PartialReplyPayload, SourceReplyDeliveryMode, @@ -15,13 +14,14 @@ import type { InputProvenance } from "../../../sessions/input-provenance.js"; import type { UserTurnTranscriptRecorder } from "../../../sessions/user-turn-transcript.js"; import type { ExecElevatedDefaults, ExecToolDefaults } from "../../bash-tools.exec-types.js"; import type { AgentStreamParams, ClientToolDefinition } from "../../command/shared-types.js"; -import type { AgentInternalEvent } from "../../internal-events.js"; -import type { BlockReplyPayload } from "../../pi-embedded-payloads.js"; +import type { BlockReplyPayload } from "../../embedded-agent-payloads.js"; import type { BlockReplyChunking, ToolProgressDetailMode, ToolResultFormat, -} from "../../pi-embedded-subscribe.shared-types.js"; +} from "../../embedded-agent-subscribe.shared-types.js"; +import type { AgentInternalEvent } from "../../internal-events.js"; +import type { AgentMessage } from "../../runtime/index.js"; import type { SkillSnapshot } from "../../skills.js"; import type { SilentReplyPromptMode } from "../../system-prompt.types.js"; import type { PromptMode } from "../../system-prompt.types.js"; @@ -36,7 +36,7 @@ export type CurrentInboundPromptContext = { promptJoiner?: "\n\n" | "\n" | " "; }; -export type RunEmbeddedPiAgentParams = { +export type RunEmbeddedAgentParams = { sessionId: string; sessionKey?: string; /** Session-like key for sandbox and tool-policy resolution. Defaults to sessionKey. */ @@ -121,7 +121,7 @@ export type RunEmbeddedPiAgentParams = { modelFallbacksOverride?: string[]; /** Session-pinned embedded harness id. Prevents runtime hot-switching. */ agentHarnessId?: string; - /** Explicit runtime override selected for this turn. Unlike agentHarnessId, this may force PI. */ + /** Explicit runtime override selected for this turn. Unlike agentHarnessId, this may force OpenClaw. */ agentHarnessRuntimeOverride?: string; authProfileId?: string; authProfileIdSource?: "auto" | "user"; diff --git a/src/agents/pi-embedded-runner/run/payloads.errors.test.ts b/src/agents/embedded-agent-runner/run/payloads.errors.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/payloads.errors.test.ts rename to src/agents/embedded-agent-runner/run/payloads.errors.test.ts index c4e819a18d2..c3e76d03617 100644 --- a/src/agents/pi-embedded-runner/run/payloads.errors.test.ts +++ b/src/agents/embedded-agent-runner/run/payloads.errors.test.ts @@ -1,7 +1,7 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { getReplyPayloadMetadata } from "../../../auto-reply/reply-payload.js"; -import { formatBillingErrorMessage } from "../../pi-embedded-helpers.js"; +import { formatBillingErrorMessage } from "../../embedded-agent-helpers.js"; import { makeAssistantMessageFixture } from "../../test-helpers/assistant-message-fixtures.js"; import { buildPayloads, diff --git a/src/agents/pi-embedded-runner/run/payloads.test-helpers.ts b/src/agents/embedded-agent-runner/run/payloads.test-helpers.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/payloads.test-helpers.ts rename to src/agents/embedded-agent-runner/run/payloads.test-helpers.ts diff --git a/src/agents/pi-embedded-runner/run/payloads.test.ts b/src/agents/embedded-agent-runner/run/payloads.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/payloads.test.ts rename to src/agents/embedded-agent-runner/run/payloads.test.ts index 1d2fb9ac16b..c3f2568cf8d 100644 --- a/src/agents/pi-embedded-runner/run/payloads.test.ts +++ b/src/agents/embedded-agent-runner/run/payloads.test.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { getReplyPayloadMetadata } from "../../../auto-reply/reply-payload.js"; import type { InteractiveReply, MessagePresentation } from "../../../interactive/payload.js"; diff --git a/src/agents/pi-embedded-runner/run/payloads.ts b/src/agents/embedded-agent-runner/run/payloads.ts similarity index 98% rename from src/agents/pi-embedded-runner/run/payloads.ts rename to src/agents/embedded-agent-runner/run/payloads.ts index 21d47fd68a7..3e8c1a33787 100644 --- a/src/agents/pi-embedded-runner/run/payloads.ts +++ b/src/agents/embedded-agent-runner/run/payloads.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import type { SourceReplyDeliveryMode } from "../../../auto-reply/get-reply-options.types.js"; import { createHeartbeatToolResponsePayload, @@ -29,10 +29,13 @@ import { getApiErrorPayloadFingerprint, isRawApiErrorPayload, normalizeTextForComparison, -} from "../../pi-embedded-helpers.js"; -import type { MessagingToolSourceReplyPayload } from "../../pi-embedded-messaging.types.js"; -import type { ToolResultFormat } from "../../pi-embedded-subscribe.shared-types.js"; -import { extractAssistantThinking, extractAssistantVisibleText } from "../../pi-embedded-utils.js"; +} from "../../embedded-agent-helpers.js"; +import type { MessagingToolSourceReplyPayload } from "../../embedded-agent-messaging.types.js"; +import type { ToolResultFormat } from "../../embedded-agent-subscribe.shared-types.js"; +import { + extractAssistantThinking, + extractAssistantVisibleText, +} from "../../embedded-agent-utils.js"; import { isExecLikeToolName, type ToolErrorSummary } from "../../tool-error-summary.js"; import { isLikelyMutatingToolName } from "../../tool-mutation.js"; diff --git a/src/agents/pi-embedded-runner/run/preemptive-compaction.test.ts b/src/agents/embedded-agent-runner/run/preemptive-compaction.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/preemptive-compaction.test.ts rename to src/agents/embedded-agent-runner/run/preemptive-compaction.test.ts index 4a7100df5f4..446d192c98f 100644 --- a/src/agents/pi-embedded-runner/run/preemptive-compaction.test.ts +++ b/src/agents/embedded-agent-runner/run/preemptive-compaction.test.ts @@ -1,6 +1,6 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { beforeAll, describe, expect, it, vi } from "vitest"; -import "../../test-helpers/pi-coding-agent-token-mock.js"; +import "../../test-helpers/agent-session-token-mock.js"; import { estimateToolResultReductionPotential } from "../tool-result-truncation.js"; let PREEMPTIVE_OVERFLOW_ERROR_TEXT: typeof import("./preemptive-compaction.js").PREEMPTIVE_OVERFLOW_ERROR_TEXT; diff --git a/src/agents/pi-embedded-runner/run/preemptive-compaction.ts b/src/agents/embedded-agent-runner/run/preemptive-compaction.ts similarity index 99% rename from src/agents/pi-embedded-runner/run/preemptive-compaction.ts rename to src/agents/embedded-agent-runner/run/preemptive-compaction.ts index 53fa4616f4d..7029818e982 100644 --- a/src/agents/pi-embedded-runner/run/preemptive-compaction.ts +++ b/src/agents/embedded-agent-runner/run/preemptive-compaction.ts @@ -1,12 +1,12 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { SessionContextBudgetStatus } from "../../../config/sessions.js"; import { isRecord } from "../../../shared/record-coerce.js"; import { estimateStringChars } from "../../../utils/cjk-chars.js"; -import { SAFETY_MARGIN } from "../../compaction.js"; import { MIN_PROMPT_BUDGET_RATIO, MIN_PROMPT_BUDGET_TOKENS, -} from "../../pi-compaction-constants.js"; +} from "../../agent-compaction-constants.js"; +import { SAFETY_MARGIN } from "../../compaction.js"; +import type { AgentMessage } from "../../runtime/index.js"; import { estimateToolResultReductionPotential } from "../tool-result-truncation.js"; import type { PreemptiveCompactionRoute } from "./preemptive-compaction.types.js"; diff --git a/src/agents/pi-embedded-runner/run/preemptive-compaction.types.ts b/src/agents/embedded-agent-runner/run/preemptive-compaction.types.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/preemptive-compaction.types.ts rename to src/agents/embedded-agent-runner/run/preemptive-compaction.types.ts diff --git a/src/agents/pi-embedded-runner/run/retry-limit.ts b/src/agents/embedded-agent-runner/run/retry-limit.ts similarity index 90% rename from src/agents/pi-embedded-runner/run/retry-limit.ts rename to src/agents/embedded-agent-runner/run/retry-limit.ts index 6be80e54703..1206460d543 100644 --- a/src/agents/pi-embedded-runner/run/retry-limit.ts +++ b/src/agents/embedded-agent-runner/run/retry-limit.ts @@ -1,6 +1,6 @@ import { FailoverError, resolveFailoverStatus } from "../../failover-error.js"; import type { EmbeddedRunLivenessState } from "../types.js"; -import type { EmbeddedPiAgentMeta, EmbeddedPiRunResult } from "../types.js"; +import type { EmbeddedAgentMeta, EmbeddedAgentRunResult } from "../types.js"; import type { RetryLimitFailoverDecision } from "./failover-policy.js"; export function handleRetryLimitExhaustion(params: { @@ -10,10 +10,10 @@ export function handleRetryLimitExhaustion(params: { model: string; profileId?: string; durationMs: number; - agentMeta: EmbeddedPiAgentMeta; + agentMeta: EmbeddedAgentMeta; replayInvalid?: boolean; livenessState?: EmbeddedRunLivenessState; -}): EmbeddedPiRunResult { +}): EmbeddedAgentRunResult { if (params.decision.action === "fallback_model") { throw new FailoverError(params.message, { reason: params.decision.reason, diff --git a/src/agents/pi-embedded-runner/run/runtime-context-prompt.test.ts b/src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/runtime-context-prompt.test.ts rename to src/agents/embedded-agent-runner/run/runtime-context-prompt.test.ts diff --git a/src/agents/pi-embedded-runner/run/runtime-context-prompt.ts b/src/agents/embedded-agent-runner/run/runtime-context-prompt.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/runtime-context-prompt.ts rename to src/agents/embedded-agent-runner/run/runtime-context-prompt.ts diff --git a/src/agents/pi-embedded-runner/run/setup.test.ts b/src/agents/embedded-agent-runner/run/setup.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/setup.test.ts rename to src/agents/embedded-agent-runner/run/setup.test.ts diff --git a/src/agents/pi-embedded-runner/run/setup.ts b/src/agents/embedded-agent-runner/run/setup.ts similarity index 96% rename from src/agents/pi-embedded-runner/run/setup.ts rename to src/agents/embedded-agent-runner/run/setup.ts index 2d22e7320eb..5053d61bccb 100644 --- a/src/agents/pi-embedded-runner/run/setup.ts +++ b/src/agents/embedded-agent-runner/run/setup.ts @@ -15,7 +15,7 @@ import { import { DEFAULT_CONTEXT_TOKENS } from "../../defaults.js"; import { FailoverError } from "../../failover-error.js"; import { log } from "../logger.js"; -import { readPiModelContextTokens } from "../model-context-tokens.js"; +import { readAgentModelContextTokens } from "../model-context-tokens.js"; type HookContext = { agentId?: string; @@ -128,12 +128,12 @@ export function resolveEffectiveRuntimeModel(params: { cfg: params.cfg, provider: params.contextConfigProvider ?? params.provider, modelId: params.modelId, - modelContextTokens: readPiModelContextTokens(params.runtimeModel), + modelContextTokens: readAgentModelContextTokens(params.runtimeModel), modelContextWindow: params.runtimeModel.contextWindow, defaultTokens: DEFAULT_CONTEXT_TOKENS, }); - // Apply contextTokens cap to model so pi-coding-agent's auto-compaction + // Apply contextTokens cap to model so session runtime's auto-compaction // threshold uses the effective limit, not the native context window. const effectiveModel = ctxInfo.tokens < (params.runtimeModel.contextWindow ?? Infinity) diff --git a/src/agents/pi-embedded-runner/run/stream-wrapper.ts b/src/agents/embedded-agent-runner/run/stream-wrapper.ts similarity index 94% rename from src/agents/pi-embedded-runner/run/stream-wrapper.ts rename to src/agents/embedded-agent-runner/run/stream-wrapper.ts index 7224cf51146..4529970590f 100644 --- a/src/agents/pi-embedded-runner/run/stream-wrapper.ts +++ b/src/agents/embedded-agent-runner/run/stream-wrapper.ts @@ -1,4 +1,4 @@ -import { streamSimple } from "@earendil-works/pi-ai"; +import { streamSimple } from "openclaw/plugin-sdk/llm"; import { createStreamIteratorWrapper } from "../../stream-iterator-wrapper.js"; type SimpleStream = ReturnType; diff --git a/src/agents/pi-embedded-runner/run/tool-media-payloads.test.ts b/src/agents/embedded-agent-runner/run/tool-media-payloads.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/tool-media-payloads.test.ts rename to src/agents/embedded-agent-runner/run/tool-media-payloads.test.ts diff --git a/src/agents/pi-embedded-runner/run/tool-media-payloads.ts b/src/agents/embedded-agent-runner/run/tool-media-payloads.ts similarity index 80% rename from src/agents/pi-embedded-runner/run/tool-media-payloads.ts rename to src/agents/embedded-agent-runner/run/tool-media-payloads.ts index 86e23aa824f..542b262f6f4 100644 --- a/src/agents/pi-embedded-runner/run/tool-media-payloads.ts +++ b/src/agents/embedded-agent-runner/run/tool-media-payloads.ts @@ -3,13 +3,9 @@ import { copyReplyPayloadMetadata, getReplyPayloadMetadata, } from "../../../auto-reply/reply-payload.js"; -import { - normalizeUniqueStringEntries, - uniqueStrings, -} from "../../../shared/string-normalization.js"; -import type { EmbeddedPiRunResult } from "../types.js"; +import type { EmbeddedAgentRunResult } from "../types.js"; -type EmbeddedRunPayload = NonNullable[number]; +type EmbeddedRunPayload = NonNullable[number]; export function mergeAttemptToolMediaPayloads(params: { payloads?: EmbeddedRunPayload[]; @@ -18,7 +14,9 @@ export function mergeAttemptToolMediaPayloads(params: { toolTrustedLocalMedia?: boolean; sourceReplyDeliveryMode?: SourceReplyDeliveryMode; }): EmbeddedRunPayload[] | undefined { - const mediaUrls = normalizeUniqueStringEntries(params.toolMediaUrls); + const mediaUrls = Array.from( + new Set(params.toolMediaUrls?.map((url) => url.trim()).filter(Boolean) ?? []), + ); if (mediaUrls.length === 0 && !params.toolAudioAsVoice && !params.toolTrustedLocalMedia) { return params.payloads; } @@ -33,7 +31,7 @@ export function mergeAttemptToolMediaPayloads(params: { ) { return payloads; } - const mergedMediaUrls = uniqueStrings([...(payload.mediaUrls ?? []), ...mediaUrls]); + const mergedMediaUrls = Array.from(new Set([...(payload.mediaUrls ?? []), ...mediaUrls])); payloads[payloadIndex] = copyReplyPayloadMetadata(payload, { ...payload, mediaUrls: mergedMediaUrls.length ? mergedMediaUrls : undefined, diff --git a/src/agents/pi-embedded-runner/run/transcript-repair-runtime-contract.test.ts b/src/agents/embedded-agent-runner/run/transcript-repair-runtime-contract.test.ts similarity index 98% rename from src/agents/pi-embedded-runner/run/transcript-repair-runtime-contract.test.ts rename to src/agents/embedded-agent-runner/run/transcript-repair-runtime-contract.test.ts index dc1418320af..9ad1942980d 100644 --- a/src/agents/pi-embedded-runner/run/transcript-repair-runtime-contract.test.ts +++ b/src/agents/embedded-agent-runner/run/transcript-repair-runtime-contract.test.ts @@ -19,7 +19,7 @@ afterEach(() => { restoreStrategy = undefined; }); -describe("Pi transcript repair runtime contract", () => { +describe("embedded agent transcript repair runtime contract", () => { it("merges text orphan leaves into the next prompt with the queued marker", () => { const result = mergeOrphanedTrailingUserPrompt({ prompt: "newest inbound message", diff --git a/src/agents/pi-embedded-runner/run/trigger-policy.test.ts b/src/agents/embedded-agent-runner/run/trigger-policy.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/trigger-policy.test.ts rename to src/agents/embedded-agent-runner/run/trigger-policy.test.ts diff --git a/src/agents/pi-embedded-runner/run/trigger-policy.ts b/src/agents/embedded-agent-runner/run/trigger-policy.ts similarity index 100% rename from src/agents/pi-embedded-runner/run/trigger-policy.ts rename to src/agents/embedded-agent-runner/run/trigger-policy.ts diff --git a/src/agents/pi-embedded-runner/run/types.ts b/src/agents/embedded-agent-runner/run/types.ts similarity index 94% rename from src/agents/pi-embedded-runner/run/types.ts rename to src/agents/embedded-agent-runner/run/types.ts index b9b45c4dbd7..43c89449993 100644 --- a/src/agents/pi-embedded-runner/run/types.ts +++ b/src/agents/embedded-agent-runner/run/types.ts @@ -1,6 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai"; -import type { AuthStorage, ModelRegistry } from "@earendil-works/pi-coding-agent"; +import type { AssistantMessage, Model } from "openclaw/plugin-sdk/llm"; import type { HeartbeatToolResponse } from "../../../auto-reply/heartbeat-tool-response.js"; import type { ThinkLevel } from "../../../auto-reply/thinking.js"; import type { @@ -12,23 +10,25 @@ import type { DiagnosticTraceContext } from "../../../infra/diagnostic-trace-con import type { PluginHookBeforeAgentStartResult } from "../../../plugins/hook-before-agent-start.types.js"; import type { AgentHarnessTaskRuntimeScope } from "../../../tasks/agent-harness-task-runtime-scope.js"; import type { AcceptedSessionSpawn } from "../../accepted-session-spawn.js"; +import type { ToolOutcomeObserver } from "../../agent-tools.before-tool-call.js"; import type { AuthProfileStore } from "../../auth-profiles/types.js"; import type { MessagingToolSend, MessagingToolSourceReplyPayload, -} from "../../pi-embedded-messaging.types.js"; -import type { ToolOutcomeObserver } from "../../pi-tools.before-tool-call.js"; +} from "../../embedded-agent-messaging.types.js"; import type { AgentRunTimeoutPhase } from "../../run-timeout-attribution.js"; import type { AgentRuntimePlan } from "../../runtime-plan/types.js"; +import type { AgentMessage } from "../../runtime/index.js"; +import type { AuthStorage, ModelRegistry } from "../../sessions/index.js"; import type { ToolErrorSummary } from "../../tool-error-summary.js"; import type { NormalizedUsage } from "../../usage.js"; import type { EmbeddedRunReplayMetadata, EmbeddedRunReplayState } from "../replay-state.js"; import type { EmbeddedRunLivenessState } from "../types.js"; -import type { RunEmbeddedPiAgentParams } from "./params.js"; +import type { RunEmbeddedAgentParams } from "./params.js"; import type { PreemptiveCompactionRoute } from "./preemptive-compaction.types.js"; type EmbeddedRunAttemptBase = Omit< - RunEmbeddedPiAgentParams, + RunEmbeddedAgentParams, "provider" | "model" | "authProfileId" | "authProfileIdSource" | "thinkLevel" | "lane" | "enqueue" >; @@ -62,7 +62,7 @@ export type EmbeddedRunAttemptParams = EmbeddedRunAttemptBase & { agentHarnessTaskRuntimeScope?: AgentHarnessTaskRuntimeScope; /** Live observer called after wrapped tool outcomes are recorded. */ onToolOutcome?: ToolOutcomeObserver; - model: Model; + model: Model; authStorage: AuthStorage; /** Auth profile store already resolved during startup for this attempt. */ authProfileStore: AuthProfileStore; diff --git a/src/agents/pi-embedded-runner/runs.test.ts b/src/agents/embedded-agent-runner/runs.test.ts similarity index 88% rename from src/agents/pi-embedded-runner/runs.test.ts rename to src/agents/embedded-agent-runner/runs.test.ts index 3e52cca2fca..33a09393819 100644 --- a/src/agents/pi-embedded-runner/runs.test.ts +++ b/src/agents/embedded-agent-runner/runs.test.ts @@ -15,15 +15,15 @@ import { import { diagnosticLogger } from "../../logging/diagnostic.js"; import { testing, - abortAndDrainEmbeddedPiRun, - abortEmbeddedPiRun, + abortAndDrainEmbeddedAgentRun, + abortEmbeddedAgentRun, clearActiveEmbeddedRun, consumeEmbeddedRunModelSwitch, getActiveEmbeddedRunSnapshot, - isEmbeddedPiRunHandleActive, - formatEmbeddedPiQueueFailureSummary, - queueEmbeddedPiMessageWithOutcome, - queueEmbeddedPiMessageWithOutcomeAsync, + isEmbeddedAgentRunHandleActive, + formatEmbeddedAgentQueueFailureSummary, + queueEmbeddedAgentMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcomeAsync, requestEmbeddedRunModelSwitch, resolveActiveEmbeddedRunHandleSessionId, resolveActiveEmbeddedRunHandleSessionIdBySessionFile, @@ -53,7 +53,7 @@ function createRunHandle( }; } -describe("pi-embedded runner run registry", () => { +describe("embedded-agent runner run registry", () => { afterEach(() => { testing.resetActiveEmbeddedRuns(); replyRunTesting.resetReplyRunRegistry(); @@ -73,7 +73,7 @@ describe("pi-embedded runner run registry", () => { setActiveEmbeddedRun("session-normal", createRunHandle({ abort: abortNormal })); - const aborted = abortEmbeddedPiRun(undefined, { mode: "compacting" }); + const aborted = abortEmbeddedAgentRun(undefined, { mode: "compacting" }); expect(aborted).toBe(true); expect(abortCompacting).toHaveBeenCalledTimes(1); expect(abortNormal).not.toHaveBeenCalled(); @@ -87,7 +87,7 @@ describe("pi-embedded runner run registry", () => { setActiveEmbeddedRun("session-b", createRunHandle({ abort: abortB })); - const aborted = abortEmbeddedPiRun(undefined, { mode: "all" }); + const aborted = abortEmbeddedAgentRun(undefined, { mode: "all" }); expect(aborted).toBe(true); expect(abortA).toHaveBeenCalledTimes(1); expect(abortB).toHaveBeenCalledTimes(1); @@ -145,7 +145,7 @@ describe("pi-embedded runner run registry", () => { }); expect( - queueEmbeddedPiMessageWithOutcome("session-steer", "continue", { + queueEmbeddedAgentMessageWithOutcome("session-steer", "continue", { steeringMode: "all", sourceReplyDeliveryMode: "message_tool_only", }).queued, @@ -164,7 +164,7 @@ describe("pi-embedded runner run registry", () => { queueMessage, }); - const outcome = queueEmbeddedPiMessageWithOutcome( + const outcome = queueEmbeddedAgentMessageWithOutcome( "session-automatic-source-reply", "continue", { @@ -189,7 +189,7 @@ describe("pi-embedded runner run registry", () => { queueMessage, }); - expect(queueEmbeddedPiMessageWithOutcome("session-default-steer", "continue").queued).toBe( + expect(queueEmbeddedAgentMessageWithOutcome("session-default-steer", "continue").queued).toBe( true, ); @@ -197,7 +197,7 @@ describe("pi-embedded runner run registry", () => { }); it("returns a structured no-active-run queue failure", () => { - const outcome = queueEmbeddedPiMessageWithOutcome("session-missing", "continue"); + const outcome = queueEmbeddedAgentMessageWithOutcome("session-missing", "continue"); expect(outcome).toEqual({ queued: false, @@ -205,7 +205,7 @@ describe("pi-embedded runner run registry", () => { reason: "no_active_run", gatewayHealth: "live", }); - expect(formatEmbeddedPiQueueFailureSummary(outcome)).toBe( + expect(formatEmbeddedAgentQueueFailureSummary(outcome)).toBe( "queue_message_failed reason=no_active_run sessionId=session-missing gatewayHealth=live", ); }); @@ -214,13 +214,13 @@ describe("pi-embedded runner run registry", () => { setActiveEmbeddedRun("session-not-streaming", createRunHandle({ isStreaming: false })); setActiveEmbeddedRun("session-compacting", createRunHandle({ isCompacting: true })); - expect(queueEmbeddedPiMessageWithOutcome("session-not-streaming", "continue")).toEqual({ + expect(queueEmbeddedAgentMessageWithOutcome("session-not-streaming", "continue")).toEqual({ queued: false, sessionId: "session-not-streaming", reason: "not_streaming", gatewayHealth: "live", }); - expect(queueEmbeddedPiMessageWithOutcome("session-compacting", "continue")).toEqual({ + expect(queueEmbeddedAgentMessageWithOutcome("session-compacting", "continue")).toEqual({ queued: false, sessionId: "session-compacting", reason: "compacting", @@ -236,7 +236,7 @@ describe("pi-embedded runner run registry", () => { }, }); - const outcome = await queueEmbeddedPiMessageWithOutcomeAsync("session-rejected", "continue"); + const outcome = await queueEmbeddedAgentMessageWithOutcomeAsync("session-rejected", "continue"); expect(outcome).toEqual({ queued: false, @@ -245,7 +245,7 @@ describe("pi-embedded runner run registry", () => { gatewayHealth: "live", errorMessage: "cannot steer a compact turn", }); - expect(formatEmbeddedPiQueueFailureSummary(outcome)).toBe( + expect(formatEmbeddedAgentQueueFailureSummary(outcome)).toBe( "queue_message_failed reason=runtime_rejected sessionId=session-rejected gatewayHealth=live error=cannot steer a compact turn", ); }); @@ -257,7 +257,7 @@ describe("pi-embedded runner run registry", () => { queueMessage, }); - const outcome = await queueEmbeddedPiMessageWithOutcomeAsync( + const outcome = await queueEmbeddedAgentMessageWithOutcomeAsync( "session-no-transcript-wait", "continue", { waitForTranscriptCommit: true }, @@ -287,7 +287,7 @@ describe("pi-embedded runner run registry", () => { }); operation.setPhase("running"); - const outcome = await queueEmbeddedPiMessageWithOutcomeAsync( + const outcome = await queueEmbeddedAgentMessageWithOutcomeAsync( "session-reply-run", "completion from child", { waitForTranscriptCommit: true }, @@ -314,7 +314,7 @@ describe("pi-embedded runner run registry", () => { const abortRun = vi.fn(); setActiveEmbeddedRun("session-stuck", createRunHandle({ abort: abortRun }), "agent:main"); - const resultPromise = abortAndDrainEmbeddedPiRun({ + const resultPromise = abortAndDrainEmbeddedAgentRun({ sessionId: "session-stuck", sessionKey: "agent:main", settleMs: 100, @@ -326,7 +326,7 @@ describe("pi-embedded runner run registry", () => { expect(result).toEqual({ aborted: true, drained: false, forceCleared: true }); expect(abortRun).toHaveBeenCalledTimes(1); - expect(isEmbeddedPiRunHandleActive("session-stuck")).toBe(false); + expect(isEmbeddedAgentRunHandleActive("session-stuck")).toBe(false); expect(resolveActiveEmbeddedRunHandleSessionId("agent:main")).toBeUndefined(); } finally { await vi.runOnlyPendingTimersAsync(); @@ -385,10 +385,10 @@ describe("pi-embedded runner run registry", () => { try { runsA.setActiveEmbeddedRun("session-shared", handle); - expect(runsB.isEmbeddedPiRunActive("session-shared")).toBe(true); + expect(runsB.isEmbeddedAgentRunActive("session-shared")).toBe(true); runsB.clearActiveEmbeddedRun("session-shared", handle); - expect(runsA.isEmbeddedPiRunActive("session-shared")).toBe(false); + expect(runsA.isEmbeddedAgentRunActive("session-shared")).toBe(false); } finally { runsA.testing.resetActiveEmbeddedRuns(); runsB.testing.resetActiveEmbeddedRuns(); @@ -398,17 +398,17 @@ describe("pi-embedded runner run registry", () => { it("tracks actual embedded handles separately from reply-operation ownership", () => { const handle = createRunHandle(); - expect(isEmbeddedPiRunHandleActive("session-a")).toBe(false); + expect(isEmbeddedAgentRunHandleActive("session-a")).toBe(false); expect(resolveActiveEmbeddedRunHandleSessionId("agent:main:main")).toBeUndefined(); setActiveEmbeddedRun("session-a", handle, "agent:main:main"); - expect(isEmbeddedPiRunHandleActive("session-a")).toBe(true); + expect(isEmbeddedAgentRunHandleActive("session-a")).toBe(true); expect(resolveActiveEmbeddedRunHandleSessionId("agent:main:main")).toBe("session-a"); clearActiveEmbeddedRun("session-a", handle, "agent:main:main"); - expect(isEmbeddedPiRunHandleActive("session-a")).toBe(false); + expect(isEmbeddedAgentRunHandleActive("session-a")).toBe(false); expect(resolveActiveEmbeddedRunHandleSessionId("agent:main:main")).toBeUndefined(); }); @@ -420,7 +420,7 @@ describe("pi-embedded runner run registry", () => { clearActiveEmbeddedRun("session-repeat-clear", handle, "agent:main:main"); clearActiveEmbeddedRun("session-repeat-clear", handle, "agent:main:main"); - expect(isEmbeddedPiRunHandleActive("session-repeat-clear")).toBe(false); + expect(isEmbeddedAgentRunHandleActive("session-repeat-clear")).toBe(false); expect(resolveActiveEmbeddedRunHandleSessionId("agent:main:main")).toBeUndefined(); expect( debugSpy.mock.calls.some(([message]) => message.includes("reason=handle_mismatch")), @@ -435,7 +435,7 @@ describe("pi-embedded runner run registry", () => { setActiveEmbeddedRun("session-handle-replaced", activeHandle); clearActiveEmbeddedRun("session-handle-replaced", staleHandle); - expect(isEmbeddedPiRunHandleActive("session-handle-replaced")).toBe(true); + expect(isEmbeddedAgentRunHandleActive("session-handle-replaced")).toBe(true); expect( debugSpy.mock.calls.some(([message]) => message.includes("reason=handle_mismatch")), ).toBe(true); diff --git a/src/agents/pi-embedded-runner/runs.ts b/src/agents/embedded-agent-runner/runs.ts similarity index 87% rename from src/agents/pi-embedded-runner/runs.ts rename to src/agents/embedded-agent-runner/runs.ts index 6915f333051..54e354f47c3 100644 --- a/src/agents/pi-embedded-runner/runs.ts +++ b/src/agents/embedded-agent-runner/runs.ts @@ -28,8 +28,8 @@ import { EMBEDDED_RUN_WAITERS, getActiveEmbeddedRunCount, type ActiveEmbeddedRunSnapshot, - type EmbeddedPiQueueHandle, - type EmbeddedPiQueueMessageOptions, + type EmbeddedAgentQueueHandle, + type EmbeddedAgentQueueMessageOptions, type EmbeddedRunModelSwitchRequest, type EmbeddedRunWaiter, } from "./run-state.js"; @@ -40,12 +40,12 @@ export { listActiveEmbeddedRunSessionIds, listActiveEmbeddedRunSessionKeys, type ActiveEmbeddedRunSnapshot, - type EmbeddedPiQueueHandle, - type EmbeddedPiQueueMessageOptions, + type EmbeddedAgentQueueHandle, + type EmbeddedAgentQueueMessageOptions, type EmbeddedRunModelSwitchRequest, } from "./run-state.js"; -export type EmbeddedPiQueueFailureReason = +export type EmbeddedAgentQueueFailureReason = | "no_active_run" | "not_streaming" | "compacting" @@ -53,7 +53,7 @@ export type EmbeddedPiQueueFailureReason = | "transcript_commit_wait_unsupported" | "runtime_rejected"; -export type EmbeddedPiQueueMessageOutcome = +export type EmbeddedAgentQueueMessageOutcome = | { queued: true; sessionId: string; @@ -65,26 +65,26 @@ export type EmbeddedPiQueueMessageOutcome = | { queued: false; sessionId: string; - reason: EmbeddedPiQueueFailureReason; + reason: EmbeddedAgentQueueFailureReason; gatewayHealth: "live"; errorMessage?: string; }; -type PreparedEmbeddedPiQueueMessage = +type PreparedEmbeddedAgentQueueMessage = | { kind: "complete"; - outcome: EmbeddedPiQueueMessageOutcome; + outcome: EmbeddedAgentQueueMessageOutcome; } | { kind: "embedded_run"; - handle: EmbeddedPiQueueHandle; + handle: EmbeddedAgentQueueHandle; }; function createQueueFailureOutcome( sessionId: string, - reason: EmbeddedPiQueueFailureReason, + reason: EmbeddedAgentQueueFailureReason, errorMessage?: string, -): EmbeddedPiQueueMessageOutcome { +): EmbeddedAgentQueueMessageOutcome { return { queued: false, sessionId, @@ -94,8 +94,8 @@ function createQueueFailureOutcome( }; } -export function formatEmbeddedPiQueueFailureSummary( - outcome: EmbeddedPiQueueMessageOutcome, +export function formatEmbeddedAgentQueueFailureSummary( + outcome: EmbeddedAgentQueueMessageOutcome, ): string | undefined { if (outcome.queued) { return undefined; @@ -152,33 +152,33 @@ function clearActiveRunSessionFiles(sessionId: string, sessionFile?: string): vo } /** - * @deprecated Use queueEmbeddedPiMessageWithOutcomeAsync for delivery decisions. + * @deprecated Use queueEmbeddedAgentMessageWithOutcomeAsync for delivery decisions. * This boolean helper only reports immediate queue eligibility; it cannot surface * async runtime rejection from the active run. */ -export function queueEmbeddedPiMessage( +export function queueEmbeddedAgentMessage( sessionId: string, text: string, - options?: EmbeddedPiQueueMessageOptions, + options?: EmbeddedAgentQueueMessageOptions, ): boolean { - return queueEmbeddedPiMessageWithOutcome(sessionId, text, options).queued; + return queueEmbeddedAgentMessageWithOutcome(sessionId, text, options).queued; } /** - * @deprecated Prefer queueEmbeddedPiMessageWithOutcomeAsync when callers need to + * @deprecated Prefer queueEmbeddedAgentMessageWithOutcomeAsync when callers need to * know whether steering was accepted. This sync helper is fire-and-forget after * initial eligibility and only logs later runtime rejection. */ -export function queueEmbeddedPiMessageWithOutcome( +export function queueEmbeddedAgentMessageWithOutcome( sessionId: string, text: string, - options?: EmbeddedPiQueueMessageOptions, -): EmbeddedPiQueueMessageOutcome { - const prepared = prepareEmbeddedPiQueueMessage(sessionId, text, options); + options?: EmbeddedAgentQueueMessageOptions, +): EmbeddedAgentQueueMessageOutcome { + const prepared = prepareEmbeddedAgentQueueMessage(sessionId, text, options); if (prepared.kind === "complete") { return prepared.outcome; } - logMessageQueued({ sessionId, source: "pi-embedded-runner" }); + logMessageQueued({ sessionId, source: "embedded-agent-runner" }); void prepared.handle .queueMessage(text, options ?? { steeringMode: "all" }) .catch((err: unknown) => { @@ -199,12 +199,12 @@ function formatQueueError(err: unknown): string { return err instanceof Error ? err.message : String(err); } -export async function queueEmbeddedPiMessageWithOutcomeAsync( +export async function queueEmbeddedAgentMessageWithOutcomeAsync( sessionId: string, text: string, - options?: EmbeddedPiQueueMessageOptions, -): Promise { - const prepared = prepareEmbeddedPiQueueMessage(sessionId, text, options); + options?: EmbeddedAgentQueueMessageOptions, +): Promise { + const prepared = prepareEmbeddedAgentQueueMessage(sessionId, text, options); if (prepared.kind === "complete") { return prepared.outcome; } @@ -212,7 +212,7 @@ export async function queueEmbeddedPiMessageWithOutcomeAsync( const enqueuedAtMs = Date.now(); await prepared.handle.queueMessage(text, options ?? { steeringMode: "all" }); const deliveredAtMs = options?.waitForTranscriptCommit ? Date.now() : undefined; - logMessageQueued({ sessionId, source: "pi-embedded-runner" }); + logMessageQueued({ sessionId, source: "embedded-agent-runner" }); return { queued: true, sessionId, @@ -228,16 +228,16 @@ export async function queueEmbeddedPiMessageWithOutcomeAsync( } } -function prepareEmbeddedPiQueueMessage( +function prepareEmbeddedAgentQueueMessage( sessionId: string, text: string, - options?: EmbeddedPiQueueMessageOptions, -): PreparedEmbeddedPiQueueMessage { + options?: EmbeddedAgentQueueMessageOptions, +): PreparedEmbeddedAgentQueueMessage { const handle = ACTIVE_EMBEDDED_RUNS.get(sessionId); if (!handle) { const queuedReplyRunMessage = queueReplyRunMessage(sessionId, text); if (queuedReplyRunMessage) { - logMessageQueued({ sessionId, source: "pi-embedded-runner" }); + logMessageQueued({ sessionId, source: "embedded-agent-runner" }); return { kind: "complete", outcome: { @@ -294,17 +294,17 @@ function prepareEmbeddedPiQueueMessage( } /** - * Abort embedded PI runs. + * Abort embedded OpenClaw runs. * * - With a sessionId, aborts that single run. * - With no sessionId, supports targeted abort modes (for example, compacting runs only). */ -export function abortEmbeddedPiRun(sessionId: string): boolean; -export function abortEmbeddedPiRun( +export function abortEmbeddedAgentRun(sessionId: string): boolean; +export function abortEmbeddedAgentRun( sessionId: undefined, opts: { mode: "all" | "compacting" }, ): boolean; -export function abortEmbeddedPiRun( +export function abortEmbeddedAgentRun( sessionId?: string, opts?: { mode?: "all" | "compacting" }, ): boolean { @@ -362,7 +362,7 @@ export function abortEmbeddedPiRun( return false; } -export function isEmbeddedPiRunActive(sessionId: string): boolean { +export function isEmbeddedAgentRunActive(sessionId: string): boolean { const active = ACTIVE_EMBEDDED_RUNS.has(sessionId) || isReplyRunActiveForSessionId(sessionId); if (active) { diag.debug(`run active check: sessionId=${sessionId} active=true`); @@ -370,7 +370,7 @@ export function isEmbeddedPiRunActive(sessionId: string): boolean { return active; } -export function isEmbeddedPiRunHandleActive(sessionId: string): boolean { +export function isEmbeddedAgentRunHandleActive(sessionId: string): boolean { const active = ACTIVE_EMBEDDED_RUNS.has(sessionId); if (active) { diag.debug(`run handle active check: sessionId=${sessionId} active=true`); @@ -378,7 +378,7 @@ export function isEmbeddedPiRunHandleActive(sessionId: string): boolean { return active; } -export function isEmbeddedPiRunStreaming(sessionId: string): boolean { +export function isEmbeddedAgentRunStreaming(sessionId: string): boolean { const handle = ACTIVE_EMBEDDED_RUNS.get(sessionId); if (!handle) { return isReplyRunStreamingForSessionId(sessionId); @@ -503,7 +503,10 @@ export async function waitForActiveEmbeddedRuns( } } -export function waitForEmbeddedPiRunEnd(sessionId: string, timeoutMs = 15_000): Promise { +export function waitForEmbeddedAgentRunEnd( + sessionId: string, + timeoutMs = 15_000, +): Promise { if (!sessionId) { return Promise.resolve(true); } @@ -540,25 +543,25 @@ export function waitForEmbeddedPiRunEnd(sessionId: string, timeoutMs = 15_000): }); } -export type AbortAndDrainEmbeddedPiRunResult = { +export type AbortAndDrainEmbeddedAgentRunResult = { aborted: boolean; drained: boolean; forceCleared: boolean; }; -export async function abortAndDrainEmbeddedPiRun(params: { +export async function abortAndDrainEmbeddedAgentRun(params: { sessionId: string; sessionKey?: string; settleMs?: number; forceClear?: boolean; reason?: string; -}): Promise { +}): Promise { const settleMs = params.settleMs ?? 15_000; - const aborted = abortEmbeddedPiRun(params.sessionId); - const drained = aborted ? await waitForEmbeddedPiRunEnd(params.sessionId, settleMs) : false; + const aborted = abortEmbeddedAgentRun(params.sessionId); + const drained = aborted ? await waitForEmbeddedAgentRunEnd(params.sessionId, settleMs) : false; const forceCleared = params.forceClear === true && (!aborted || !drained) - ? forceClearEmbeddedPiRun(params.sessionId, params.sessionKey, params.reason) + ? forceClearEmbeddedAgentRun(params.sessionId, params.sessionKey, params.reason) : false; return { aborted, drained, forceCleared }; } @@ -578,7 +581,7 @@ function notifyEmbeddedRunEnded(sessionId: string) { export function setActiveEmbeddedRun( sessionId: string, - handle: EmbeddedPiQueueHandle, + handle: EmbeddedAgentQueueHandle, sessionKey?: string, sessionFile?: string, ) { @@ -624,7 +627,7 @@ export function updateActiveEmbeddedRunSessionFile( export function clearActiveEmbeddedRun( sessionId: string, - handle: EmbeddedPiQueueHandle, + handle: EmbeddedAgentQueueHandle, sessionKey?: string, sessionFile?: string, ) { @@ -655,7 +658,7 @@ export function clearActiveEmbeddedRun( } } -export function forceClearEmbeddedPiRun( +export function forceClearEmbeddedAgentRun( sessionId: string, sessionKey?: string, reason = "stuck_recovery", diff --git a/src/agents/pi-embedded-runner/sandbox-info.ts b/src/agents/embedded-agent-runner/sandbox-info.ts similarity index 100% rename from src/agents/pi-embedded-runner/sandbox-info.ts rename to src/agents/embedded-agent-runner/sandbox-info.ts diff --git a/src/agents/pi-embedded-runner/sanitize-session-history.tool-result-details.test.ts b/src/agents/embedded-agent-runner/sanitize-session-history.tool-result-details.test.ts similarity index 93% rename from src/agents/pi-embedded-runner/sanitize-session-history.tool-result-details.test.ts rename to src/agents/embedded-agent-runner/sanitize-session-history.tool-result-details.test.ts index 6cc03993154..4268d39dcc0 100644 --- a/src/agents/pi-embedded-runner/sanitize-session-history.tool-result-details.test.ts +++ b/src/agents/embedded-agent-runner/sanitize-session-history.tool-result-details.test.ts @@ -1,6 +1,6 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { ToolResultMessage, UserMessage } from "@earendil-works/pi-ai"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; +import type { ToolResultMessage, UserMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import { makeAgentAssistantMessage } from "../test-helpers/agent-message-fixtures.js"; import { sanitizeSessionHistory } from "./replay-history.js"; diff --git a/src/agents/pi-embedded-runner/session-file-key.ts b/src/agents/embedded-agent-runner/session-file-key.ts similarity index 100% rename from src/agents/pi-embedded-runner/session-file-key.ts rename to src/agents/embedded-agent-runner/session-file-key.ts diff --git a/src/agents/pi-embedded-runner/session-manager-cache.test.ts b/src/agents/embedded-agent-runner/session-manager-cache.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/session-manager-cache.test.ts rename to src/agents/embedded-agent-runner/session-manager-cache.test.ts diff --git a/src/agents/pi-embedded-runner/session-manager-cache.ts b/src/agents/embedded-agent-runner/session-manager-cache.ts similarity index 100% rename from src/agents/pi-embedded-runner/session-manager-cache.ts rename to src/agents/embedded-agent-runner/session-manager-cache.ts diff --git a/src/agents/pi-embedded-runner/session-manager-init.ts b/src/agents/embedded-agent-runner/session-manager-init.ts similarity index 97% rename from src/agents/pi-embedded-runner/session-manager-init.ts rename to src/agents/embedded-agent-runner/session-manager-init.ts index 95c699947bd..6a947430245 100644 --- a/src/agents/pi-embedded-runner/session-manager-init.ts +++ b/src/agents/embedded-agent-runner/session-manager-init.ts @@ -4,7 +4,7 @@ type SessionHeaderEntry = { type: "session"; id?: string; cwd?: string }; type SessionMessageEntry = { type: "message"; message?: { role?: string } }; /** - * pi-coding-agent SessionManager persistence quirk: + * session runtime SessionManager persistence quirk: * - If the file exists but has no assistant message, SessionManager marks itself `flushed=true` * and will never persist the initial user message. * - If the file doesn't exist yet, SessionManager builds a new session in memory and flushes diff --git a/src/agents/pi-embedded-runner/sessions-yield.orchestration.test.ts b/src/agents/embedded-agent-runner/sessions-yield.orchestration.test.ts similarity index 87% rename from src/agents/pi-embedded-runner/sessions-yield.orchestration.test.ts rename to src/agents/embedded-agent-runner/sessions-yield.orchestration.test.ts index d08ae9d1cad..c51896ac247 100644 --- a/src/agents/pi-embedded-runner/sessions-yield.orchestration.test.ts +++ b/src/agents/embedded-agent-runner/sessions-yield.orchestration.test.ts @@ -11,13 +11,13 @@ import { mockedRunEmbeddedAttempt, overflowBaseRunParams, } from "./run.overflow-compaction.harness.js"; -import { isEmbeddedPiRunActive, queueEmbeddedPiMessageWithOutcome } from "./runs.js"; +import { isEmbeddedAgentRunActive, queueEmbeddedAgentMessageWithOutcome } from "./runs.js"; -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; describe("sessions_yield orchestration", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); }); beforeEach(() => { @@ -37,7 +37,7 @@ describe("sessions_yield orchestration", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, sessionId, runId: "run-yield-orchestration", @@ -50,10 +50,10 @@ describe("sessions_yield orchestration", () => { expect(result.meta.pendingToolCalls).toBeUndefined(); // 3. Parent session is IDLE (not in ACTIVE_EMBEDDED_RUNS) - expect(isEmbeddedPiRunActive(sessionId)).toBe(false); + expect(isEmbeddedAgentRunActive(sessionId)).toBe(false); // 4. Steer would fail (message delivery must take direct path, not steer) - const queueResult = queueEmbeddedPiMessageWithOutcome(sessionId, "subagent result"); + const queueResult = queueEmbeddedAgentMessageWithOutcome(sessionId, "subagent result"); expect(queueResult.queued).toBe(false); if (queueResult.queued) { throw new Error("expected queue attempt to fail without an active run"); @@ -71,7 +71,7 @@ describe("sessions_yield orchestration", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, runId: "run-yield-vs-client-tool", }); @@ -97,7 +97,7 @@ describe("sessions_yield orchestration", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, runId: "run-multi-client-tool", }); @@ -117,7 +117,7 @@ describe("sessions_yield orchestration", () => { it("normal attempt without yield has no stopReason override", async () => { mockedRunEmbeddedAttempt.mockResolvedValueOnce(makeAttemptResult({ promptError: null })); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ ...overflowBaseRunParams, runId: "run-no-yield", }); diff --git a/src/agents/pi-embedded-runner/skills-runtime.integration.test.ts b/src/agents/embedded-agent-runner/skills-runtime.integration.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/skills-runtime.integration.test.ts rename to src/agents/embedded-agent-runner/skills-runtime.integration.test.ts diff --git a/src/agents/pi-embedded-runner/skills-runtime.test.ts b/src/agents/embedded-agent-runner/skills-runtime.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/skills-runtime.test.ts rename to src/agents/embedded-agent-runner/skills-runtime.test.ts diff --git a/src/agents/pi-embedded-runner/skills-runtime.ts b/src/agents/embedded-agent-runner/skills-runtime.ts similarity index 100% rename from src/agents/pi-embedded-runner/skills-runtime.ts rename to src/agents/embedded-agent-runner/skills-runtime.ts diff --git a/src/agents/pi-embedded-runner/stream-resolution.test.ts b/src/agents/embedded-agent-runner/stream-resolution.test.ts similarity index 89% rename from src/agents/pi-embedded-runner/stream-resolution.test.ts rename to src/agents/embedded-agent-runner/stream-resolution.test.ts index 183a6fd2bfd..e39a8b7b51f 100644 --- a/src/agents/pi-embedded-runner/stream-resolution.test.ts +++ b/src/agents/embedded-agent-runner/stream-resolution.test.ts @@ -1,5 +1,5 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { getApiProvider, streamSimple } from "@earendil-works/pi-ai"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; +import { getApiProvider, streamSimple } from "openclaw/plugin-sdk/llm"; import { afterEach, describe, expect, it, vi } from "vitest"; import * as providerTransportStream from "../provider-transport-stream.js"; import { SYSTEM_PROMPT_CACHE_BOUNDARY } from "../system-prompt-cache-boundary.js"; @@ -44,7 +44,7 @@ async function expectStreamResultRecord( } afterEach(() => { - testing.resetPiNativeCodexResponsesStreamFnForTest(); + testing.resetOpenClawNativeCodexResponsesStreamFnForTest(); }); describe("describeEmbeddedAgentStreamStrategy", () => { @@ -75,7 +75,7 @@ describe("describeEmbeddedAgentStreamStrategy", () => { ).toBe("boundary-aware:openai-responses"); }); - it("describes default Codex fallback as PI native", () => { + it("describes default Codex fallback as OpenClaw native", () => { expect( describeEmbeddedAgentStreamStrategy({ currentStreamFn: undefined, @@ -85,7 +85,7 @@ describe("describeEmbeddedAgentStreamStrategy", () => { id: "codex-mini-latest", } as never, }), - ).toBe("pi-native-codex-responses"); + ).toBe("openclaw-native-codex-responses"); }); it("keeps custom session streams labeled as custom", () => { @@ -146,9 +146,9 @@ describe("resolveEmbeddedAgentStreamFn", () => { expect(streamFn).not.toBe(streamSimple); }); - it("routes Codex responses fallbacks through PI native transport", async () => { + it("routes Codex responses fallbacks through OpenClaw native transport", async () => { const nativeStreamFn = vi.fn(async (_model, context, options) => ({ context, options })); - testing.setPiNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); + testing.setOpenClawNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); const streamFn = resolveEmbeddedAgentStreamFn({ currentStreamFn: undefined, sessionId: "session-1", @@ -188,7 +188,7 @@ describe("resolveEmbeddedAgentStreamFn", () => { expect(streamFn).not.toBe(streamSimple); }); - it("routes PI native OpenAI-compatible provider streams through boundary-aware transports", async () => { + it("routes OpenClaw native OpenAI-compatible provider streams through boundary-aware transports", async () => { const nativeStreamFn = getApiProvider("openai-completions")?.streamSimple; if (!nativeStreamFn) { throw new Error("expected native OpenAI-compatible stream function"); @@ -321,9 +321,9 @@ describe("resolveEmbeddedAgentStreamFn", () => { expect(result.signal).toBe(explicitSignal); }); - it("injects the resolved run api key into the PI native Codex Responses fallback", async () => { + it("injects the resolved run api key into the OpenClaw native Codex Responses fallback", async () => { const nativeStreamFn = vi.fn(async (_model, _context, options) => options); - testing.setPiNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); + testing.setOpenClawNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); const streamFn = resolveEmbeddedAgentStreamFn({ currentStreamFn: undefined, sessionId: "session-1", @@ -343,12 +343,12 @@ describe("resolveEmbeddedAgentStreamFn", () => { expect(nativeStreamFn).toHaveBeenCalledTimes(1); }); - it("falls back to authStorage when no resolved api key is available for PI native fallback", async () => { + it("falls back to authStorage when no resolved api key is available for OpenClaw native fallback", async () => { const nativeStreamFn = vi.fn(async (_model, _context, options) => options); const authStorage = { getApiKey: vi.fn(async () => "stored-bearer-token"), }; - testing.setPiNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); + testing.setOpenClawNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); const streamFn = resolveEmbeddedAgentStreamFn({ currentStreamFn: undefined, sessionId: "session-1", @@ -368,10 +368,10 @@ describe("resolveEmbeddedAgentStreamFn", () => { expect(authStorage.getApiKey).toHaveBeenCalledWith("openai-codex"); }); - it("forwards the run abort signal into the PI native fallback when callers omit one", async () => { + it("forwards the run abort signal into the OpenClaw native fallback when callers omit one", async () => { const nativeStreamFn = vi.fn(async (_model, _context, options) => options); const runSignal = new AbortController().signal; - testing.setPiNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); + testing.setOpenClawNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); const streamFn = resolveEmbeddedAgentStreamFn({ currentStreamFn: undefined, sessionId: "session-1", @@ -392,11 +392,11 @@ describe("resolveEmbeddedAgentStreamFn", () => { expect(result.apiKey).toBe("oauth-bearer-token"); }); - it("does not overwrite an explicit signal on the PI native fallback path", async () => { + it("does not overwrite an explicit signal on the OpenClaw native fallback path", async () => { const nativeStreamFn = vi.fn(async (_model, _context, options) => options); const runSignal = new AbortController().signal; const explicitSignal = new AbortController().signal; - testing.setPiNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); + testing.setOpenClawNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); const streamFn = resolveEmbeddedAgentStreamFn({ currentStreamFn: undefined, sessionId: "session-1", @@ -418,10 +418,10 @@ describe("resolveEmbeddedAgentStreamFn", () => { expect(result.signal).toBe(explicitSignal); }); - it("forwards the run signal on the sync PI native fallback path without auth credentials", async () => { + it("forwards the run signal on the sync OpenClaw native fallback path without auth credentials", async () => { const nativeStreamFn = vi.fn(async (_model, _context, options) => options); const runSignal = new AbortController().signal; - testing.setPiNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); + testing.setOpenClawNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); const streamFn = resolveEmbeddedAgentStreamFn({ currentStreamFn: undefined, sessionId: "session-1", @@ -440,9 +440,9 @@ describe("resolveEmbeddedAgentStreamFn", () => { expect(result.signal).toBe(runSignal); }); - it("strips cache boundary markers on the PI native fallback path", async () => { + it("strips cache boundary markers on the OpenClaw native fallback path", async () => { const nativeStreamFn = vi.fn(async (_model, context, _options) => context); - testing.setPiNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); + testing.setOpenClawNativeCodexResponsesStreamFnForTest(nativeStreamFn as never); const streamFn = resolveEmbeddedAgentStreamFn({ currentStreamFn: undefined, sessionId: "session-1", diff --git a/src/agents/pi-embedded-runner/stream-resolution.ts b/src/agents/embedded-agent-runner/stream-resolution.ts similarity index 85% rename from src/agents/pi-embedded-runner/stream-resolution.ts rename to src/agents/embedded-agent-runner/stream-resolution.ts index fabbb3ec2b0..1619d2c0e65 100644 --- a/src/agents/pi-embedded-runner/stream-resolution.ts +++ b/src/agents/embedded-agent-runner/stream-resolution.ts @@ -1,12 +1,12 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { getApiProvider, streamSimple } from "@earendil-works/pi-ai"; +import { getApiProvider, streamSimple } from "openclaw/plugin-sdk/llm"; import { createAnthropicVertexStreamFnForModel } from "../anthropic-vertex-stream.js"; import { createBoundaryAwareStreamFnForModel } from "../provider-transport-stream.js"; +import type { StreamFn } from "../runtime/index.js"; import { stripSystemPromptCacheBoundary } from "../system-prompt-cache-boundary.js"; import type { EmbeddedRunAttemptParams } from "./run/types.js"; let embeddedAgentBaseStreamFnCache = new WeakMap(); -let piNativeCodexResponsesStreamFnForTest: StreamFn | undefined; +let openClawNativeCodexResponsesStreamFnForTest: StreamFn | undefined; type EmbeddedStreamOptions = Parameters[2] & { authProfileId?: string; @@ -28,7 +28,7 @@ export function resetEmbeddedAgentBaseStreamFnCacheForTest(): void { embeddedAgentBaseStreamFnCache = new WeakMap(); } -function isDefaultPiStreamFnForModel( +function isDefaultOpenClawStreamFnForModel( model: EmbeddedRunAttemptParams["model"], streamFn: StreamFn | undefined, ): boolean { @@ -51,17 +51,17 @@ function isOpenAICodexResponsesModel(model: EmbeddedRunAttemptParams["model"]): return model.provider === "openai-codex" && model.api === "openai-codex-responses"; } -function resolvePiNativeCodexResponsesStreamFn(params: { +function resolveOpenClawNativeCodexResponsesStreamFn(params: { model: EmbeddedRunAttemptParams["model"]; currentStreamFn: StreamFn | undefined; }): StreamFn | undefined { if (!isOpenAICodexResponsesModel(params.model)) { return undefined; } - if (!isDefaultPiStreamFnForModel(params.model, params.currentStreamFn)) { + if (!isDefaultOpenClawStreamFnForModel(params.model, params.currentStreamFn)) { return undefined; } - return piNativeCodexResponsesStreamFnForTest ?? params.currentStreamFn ?? streamSimple; + return openClawNativeCodexResponsesStreamFnForTest ?? params.currentStreamFn ?? streamSimple; } export function describeEmbeddedAgentStreamStrategy(params: { @@ -77,14 +77,14 @@ export function describeEmbeddedAgentStreamStrategy(params: { return "anthropic-vertex"; } if ( - resolvePiNativeCodexResponsesStreamFn({ + resolveOpenClawNativeCodexResponsesStreamFn({ model: params.model, currentStreamFn: params.currentStreamFn, }) ) { - return "pi-native-codex-responses"; + return "openclaw-native-codex-responses"; } - if (isDefaultPiStreamFnForModel(params.model, params.currentStreamFn)) { + if (isDefaultOpenClawStreamFnForModel(params.model, params.currentStreamFn)) { return createBoundaryAwareStreamFnForModel(params.model) ? `boundary-aware:${params.model.api}` : "stream-simple"; @@ -142,12 +142,12 @@ export function resolveEmbeddedAgentStreamFn(params: { return createAnthropicVertexStreamFnForModel(params.model); } - const piNativeCodexResponsesStreamFn = resolvePiNativeCodexResponsesStreamFn({ + const openClawNativeCodexResponsesStreamFn = resolveOpenClawNativeCodexResponsesStreamFn({ model: params.model, currentStreamFn: params.currentStreamFn, }); - if (piNativeCodexResponsesStreamFn) { - return wrapEmbeddedAgentStreamFn(piNativeCodexResponsesStreamFn, { + if (openClawNativeCodexResponsesStreamFn) { + return wrapEmbeddedAgentStreamFn(openClawNativeCodexResponsesStreamFn, { runSignal: params.signal, resolvedApiKey: params.resolvedApiKey, authProfileId: params.authProfileId, @@ -165,12 +165,12 @@ export function resolveEmbeddedAgentStreamFn(params: { } if ( - isDefaultPiStreamFnForModel(params.model, params.currentStreamFn) || + isDefaultOpenClawStreamFnForModel(params.model, params.currentStreamFn) || hasResolvedRuntimeApiKey(params.resolvedApiKey) ) { const boundaryAwareStreamFn = createBoundaryAwareStreamFnForModel(params.model); if (boundaryAwareStreamFn) { - // Some PI session factories return a provider-specific stream wrapper + // Some OpenClaw session factories return a provider-specific stream wrapper // once runtime auth is resolved. Keep transport-supported APIs on // OpenClaw's HTTP transport so provider-specific auth/header semantics // are not lost behind that wrapper. @@ -193,11 +193,11 @@ export function resolveEmbeddedAgentStreamFn(params: { } export const testing = { - setPiNativeCodexResponsesStreamFnForTest(streamFn: StreamFn | undefined): void { - piNativeCodexResponsesStreamFnForTest = streamFn; + setOpenClawNativeCodexResponsesStreamFnForTest(streamFn: StreamFn | undefined): void { + openClawNativeCodexResponsesStreamFnForTest = streamFn; }, - resetPiNativeCodexResponsesStreamFnForTest(): void { - piNativeCodexResponsesStreamFnForTest = undefined; + resetOpenClawNativeCodexResponsesStreamFnForTest(): void { + openClawNativeCodexResponsesStreamFnForTest = undefined; }, }; diff --git a/src/agents/pi-embedded-runner/system-prompt.test.ts b/src/agents/embedded-agent-runner/system-prompt.test.ts similarity index 98% rename from src/agents/pi-embedded-runner/system-prompt.test.ts rename to src/agents/embedded-agent-runner/system-prompt.test.ts index 46ffb901cd3..3664612d378 100644 --- a/src/agents/pi-embedded-runner/system-prompt.test.ts +++ b/src/agents/embedded-agent-runner/system-prompt.test.ts @@ -1,4 +1,4 @@ -import type { AgentSession } from "@earendil-works/pi-coding-agent"; +import type { AgentSession } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, describe, expect, it, vi } from "vitest"; import { clearMemoryPluginState, registerMemoryPromptSection } from "../../plugins/memory-state.js"; import { @@ -207,7 +207,7 @@ describe("buildEmbeddedSystemPrompt", () => { }); expect(prompt).toContain("- sessions_spawn"); - expect(prompt).not.toContain("Pi lists the standard tools above"); + expect(prompt).not.toContain("OpenClaw lists the standard tools above"); expect(prompt).not.toContain("For long waits, avoid rapid poll loops"); expect(prompt).not.toContain("Larger work: use `sessions_spawn`"); expect(prompt).not.toContain("Do not poll `subagents list` / `sessions_list` in a loop"); diff --git a/src/agents/pi-embedded-runner/system-prompt.ts b/src/agents/embedded-agent-runner/system-prompt.ts similarity index 96% rename from src/agents/pi-embedded-runner/system-prompt.ts rename to src/agents/embedded-agent-runner/system-prompt.ts index 32c393319fd..c6d1f416479 100644 --- a/src/agents/pi-embedded-runner/system-prompt.ts +++ b/src/agents/embedded-agent-runner/system-prompt.ts @@ -1,5 +1,3 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; -import type { AgentSession } from "@earendil-works/pi-coding-agent"; import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js"; import type { SubagentDelegationMode } from "../../config/types.agent-defaults.js"; import type { MemoryCitationsMode } from "../../config/types.memory.js"; @@ -8,7 +6,9 @@ import type { AgentPromptSurfaceKind } from "../../plugins/types.js"; import type { ActiveProcessSessionReference } from "../bash-process-references.js"; import type { BootstrapMode } from "../bootstrap-mode.js"; import type { ResolvedTimeFormat } from "../date-time.js"; -import type { EmbeddedContextFile } from "../pi-embedded-helpers.js"; +import type { EmbeddedContextFile } from "../embedded-agent-helpers.js"; +import type { AgentTool } from "../runtime/index.js"; +import type { AgentSession } from "../sessions/index.js"; import { buildConfiguredAgentSystemPrompt } from "../system-prompt-config.js"; import type { ProviderSystemPromptContribution } from "../system-prompt-contribution.js"; import type { PromptMode, SilentReplyPromptMode } from "../system-prompt.types.js"; @@ -45,7 +45,7 @@ export function buildEmbeddedSystemPrompt(params: { subagentDelegationMode?: SubagentDelegationMode; /** Whether ACP-specific routing guidance should be included. Defaults to true. */ acpEnabled?: boolean; - /** Prompt surface controls runtime-specific fallback fragments. Defaults to PI main. */ + /** Prompt surface controls runtime-specific fallback fragments. Defaults to OpenClaw main. */ promptSurface?: AgentPromptSurfaceKind; /** Registered runtime slash/native command names such as `codex`. */ nativeCommandNames?: string[]; diff --git a/src/agents/pi-embedded-runner/thinking.test.ts b/src/agents/embedded-agent-runner/thinking.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/thinking.test.ts rename to src/agents/embedded-agent-runner/thinking.test.ts index 819d4dd3f51..941dcb2cad2 100644 --- a/src/agents/pi-embedded-runner/thinking.test.ts +++ b/src/agents/embedded-agent-runner/thinking.test.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { createAssistantMessageEventStream } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { castAgentMessage, castAgentMessages } from "../test-helpers/agent-message-fixtures.js"; import { diff --git a/src/agents/pi-embedded-runner/thinking.ts b/src/agents/embedded-agent-runner/thinking.ts similarity index 99% rename from src/agents/pi-embedded-runner/thinking.ts rename to src/agents/embedded-agent-runner/thinking.ts index 43cd0e5f3da..ecbebebd1ff 100644 --- a/src/agents/pi-embedded-runner/thinking.ts +++ b/src/agents/embedded-agent-runner/thinking.ts @@ -1,6 +1,6 @@ -import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core"; -import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; +import { createAssistantMessageEventStream } from "openclaw/plugin-sdk/llm"; import { formatErrorMessage } from "../../infra/errors.js"; +import type { AgentMessage, StreamFn } from "../runtime/index.js"; import { log } from "./logger.js"; type AssistantContentBlock = Extract["content"][number]; diff --git a/src/agents/pi-embedded-runner/tool-call-argument-decoding.ts b/src/agents/embedded-agent-runner/tool-call-argument-decoding.ts similarity index 96% rename from src/agents/pi-embedded-runner/tool-call-argument-decoding.ts rename to src/agents/embedded-agent-runner/tool-call-argument-decoding.ts index b61cf2150a0..9d96c237749 100644 --- a/src/agents/pi-embedded-runner/tool-call-argument-decoding.ts +++ b/src/agents/embedded-agent-runner/tool-call-argument-decoding.ts @@ -1,6 +1,6 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { streamSimple } from "@earendil-works/pi-ai"; +import { streamSimple } from "openclaw/plugin-sdk/llm"; import { visitObjectContentBlocks } from "../../shared/message-content-blocks.js"; +import type { StreamFn } from "../runtime/index.js"; const HTML_ENTITY_RE = /&(?:amp|lt|gt|quot|apos|#39|#x[0-9a-f]+|#\d+);/i; diff --git a/src/agents/pi-embedded-runner/tool-name-allowlist.test.ts b/src/agents/embedded-agent-runner/tool-name-allowlist.test.ts similarity index 87% rename from src/agents/pi-embedded-runner/tool-name-allowlist.test.ts rename to src/agents/embedded-agent-runner/tool-name-allowlist.test.ts index e3ed5de9518..ad83524cea1 100644 --- a/src/agents/pi-embedded-runner/tool-name-allowlist.test.ts +++ b/src/agents/embedded-agent-runner/tool-name-allowlist.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { findClientToolNameConflicts } from "../pi-tool-definition-adapter.js"; -import { createStubTool } from "../test-helpers/pi-tool-stubs.js"; +import { findClientToolNameConflicts } from "../agent-tool-definition-adapter.js"; +import { createStubTool } from "../test-helpers/agent-tool-stubs.js"; import { addClientToolsToToolSearchCatalog, applyToolSearchCatalog, @@ -11,7 +11,7 @@ import { collectAllowedToolNames, collectCoreBuiltinToolNames, collectRegisteredToolNames, - PI_RESERVED_TOOL_NAMES, + AGENT_RESERVED_TOOL_NAMES, toSessionToolAllowlist, } from "./tool-name-allowlist.js"; @@ -34,13 +34,13 @@ describe("tool name allowlists", () => { expect([...names]).toEqual(["read", "memory_search", "image_generate"]); }); - it("builds a stable Pi session allowlist from custom tool names", () => { + it("builds a stable agent session allowlist from custom tool names", () => { const allowlist = toSessionToolAllowlist(new Set(["write", "read", "read", "edit"])); expect(allowlist).toEqual(["edit", "read", "write"]); }); - it("collects exact registered custom-tool names for the Pi session allowlist", () => { + it("collects exact registered custom-tool names for the agent session allowlist", () => { const allowlist = toSessionToolAllowlist( collectRegisteredToolNames([ { name: "exec" }, @@ -79,16 +79,24 @@ describe("tool name allowlists", () => { }, }, ], - existingToolNames: [...names, ...PI_RESERVED_TOOL_NAMES], + existingToolNames: [...names, ...AGENT_RESERVED_TOOL_NAMES], }), ).toEqual(["exec"]); }); - it("pins the reserved Pi built-in tool namespace used by client conflict checks", () => { - expect(PI_RESERVED_TOOL_NAMES).toEqual(["bash", "edit", "find", "grep", "ls", "read", "write"]); + it("pins the reserved OpenClaw built-in tool namespace used by client conflict checks", () => { + expect(AGENT_RESERVED_TOOL_NAMES).toEqual([ + "bash", + "edit", + "find", + "grep", + "ls", + "read", + "write", + ]); }); - it("keeps collected run allowlists broader than the Pi session allowlist source", () => { + it("keeps collected run allowlists broader than the agent session allowlist source", () => { const allowlist = toSessionToolAllowlist( collectAllowedToolNames({ tools: [createStubTool("exec"), createStubTool("read"), createStubTool("exec")], diff --git a/src/agents/pi-embedded-runner/tool-name-allowlist.ts b/src/agents/embedded-agent-runner/tool-name-allowlist.ts similarity index 75% rename from src/agents/pi-embedded-runner/tool-name-allowlist.ts rename to src/agents/embedded-agent-runner/tool-name-allowlist.ts index 4fa60e2ac30..8b62add2f58 100644 --- a/src/agents/pi-embedded-runner/tool-name-allowlist.ts +++ b/src/agents/embedded-agent-runner/tool-name-allowlist.ts @@ -1,12 +1,11 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; -import { sortUniqueStrings } from "../../shared/string-normalization.js"; +import type { AgentTool } from "../runtime/index.js"; import type { ClientToolDefinition } from "./run/params.js"; /** - * Pi built-in tools that remain present in the embedded runtime even when + * OpenClaw built-in tools that remain present in the embedded runtime even when * OpenClaw routes execution through custom tool definitions. */ -export const PI_RESERVED_TOOL_NAMES = ["bash", "edit", "find", "grep", "ls", "read", "write"]; +export const AGENT_RESERVED_TOOL_NAMES = ["bash", "edit", "find", "grep", "ls", "read", "write"]; function addName(names: Set, value: unknown): void { if (typeof value !== "string") { @@ -33,7 +32,7 @@ export function collectAllowedToolNames(params: { } /** - * Collect the exact tool names registered with Pi for this session. + * Collect the exact tool names registered with the embedded agent for this session. */ export function collectRegisteredToolNames(tools: Array<{ name?: string }>): Set { const names = new Set(); @@ -58,5 +57,5 @@ export function collectCoreBuiltinToolNames( } export function toSessionToolAllowlist(allowedToolNames: Iterable): string[] { - return sortUniqueStrings(allowedToolNames); + return [...new Set(allowedToolNames)].toSorted((a, b) => a.localeCompare(b)); } diff --git a/src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts b/src/agents/embedded-agent-runner/tool-result-char-estimator.test.ts similarity index 96% rename from src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts rename to src/agents/embedded-agent-runner/tool-result-char-estimator.test.ts index de27accf7a2..27417e80eb6 100644 --- a/src/agents/pi-embedded-runner/tool-result-char-estimator.test.ts +++ b/src/agents/embedded-agent-runner/tool-result-char-estimator.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; import { createMessageCharEstimateCache, diff --git a/src/agents/pi-embedded-runner/tool-result-char-estimator.ts b/src/agents/embedded-agent-runner/tool-result-char-estimator.ts similarity index 98% rename from src/agents/pi-embedded-runner/tool-result-char-estimator.ts rename to src/agents/embedded-agent-runner/tool-result-char-estimator.ts index b12f326dbab..7c2e859d805 100644 --- a/src/agents/pi-embedded-runner/tool-result-char-estimator.ts +++ b/src/agents/embedded-agent-runner/tool-result-char-estimator.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../runtime/index.js"; export const CHARS_PER_TOKEN_ESTIMATE = 4; export const TOOL_RESULT_CHARS_PER_TOKEN_ESTIMATE = 2; diff --git a/src/agents/pi-embedded-runner/tool-result-context-guard.test.ts b/src/agents/embedded-agent-runner/tool-result-context-guard.test.ts similarity index 98% rename from src/agents/pi-embedded-runner/tool-result-context-guard.test.ts rename to src/agents/embedded-agent-runner/tool-result-context-guard.test.ts index da009b8e218..f8eeed0399e 100644 --- a/src/agents/pi-embedded-runner/tool-result-context-guard.test.ts +++ b/src/agents/embedded-agent-runner/tool-result-context-guard.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it, vi } from "vitest"; import type { ContextEngine } from "../../context-engine/types.js"; import { castAgentMessage } from "../test-helpers/agent-message-fixtures.js"; @@ -135,7 +135,7 @@ async function applyMidTurnPrecheckGuardToContext( return await agent.transformContext?.(contextForNextCall, new AbortController().signal); } -function expectPiStyleTruncation(text: string): void { +function expectOpenClawTruncation(text: string): void { expect(text).toContain(CONTEXT_LIMIT_TRUNCATION_NOTICE); expect(text).toMatch( /\[\.\.\. \d+ more characters truncated; rerun with narrower args if needed\]$/, @@ -170,7 +170,7 @@ function recordMockArg( } describe("formatContextLimitTruncationNotice", () => { - it("formats pi-style truncation wording with a count", () => { + it("formats truncation wording with a count", () => { expect(formatContextLimitTruncationNotice(123)).toBe( "[... 123 more characters truncated; rerun with narrower args if needed]", ); @@ -205,7 +205,7 @@ describe("installToolResultContextGuard", () => { expect(transformed).not.toBe(contextForNextCall); const newResultText = getToolResultText(transformed[0]); expect(newResultText.length).toBeLessThan(5_000); - expectPiStyleTruncation(newResultText); + expectOpenClawTruncation(newResultText); expect(getToolResultText(contextForNextCall[0])).toBe("z".repeat(5_000)); }); @@ -222,10 +222,10 @@ describe("installToolResultContextGuard", () => { const transformed = (await applyGuardToContext(agent, contextForNextCall)) as AgentMessage[]; expect(transformed).not.toBe(contextForNextCall); - expectPiStyleTruncation(getToolResultText(transformed[0])); + expectOpenClawTruncation(getToolResultText(transformed[0])); }); - it("handles legacy role=tool string outputs with pi-style truncation wording", async () => { + it("handles legacy role=tool string outputs with truncation wording", async () => { const agent = makeGuardableAgent(); const contextForNextCall = [makeLegacyToolResult("call_big", "y".repeat(5_000))]; @@ -233,7 +233,7 @@ describe("installToolResultContextGuard", () => { const newResultText = getToolResultText(transformed[0]); expect(typeof (transformed[0] as { content?: unknown }).content).toBe("string"); - expectPiStyleTruncation(newResultText); + expectOpenClawTruncation(newResultText); }); it("drops oversized tool-result details when truncating once", async () => { @@ -246,7 +246,7 @@ describe("installToolResultContextGuard", () => { const result = transformed[0] as { details?: unknown }; const newResultText = getToolResultText(transformed[0]); - expectPiStyleTruncation(newResultText); + expectOpenClawTruncation(newResultText); expect(result.details).toBeUndefined(); const originalDetails = (contextForNextCall[0] as { details?: { truncation?: unknown } }) .details; @@ -312,7 +312,7 @@ describe("installToolResultContextGuard", () => { 100_000, )) as AgentMessage[]; - expectPiStyleTruncation(getToolResultText(transformed[0])); + expectOpenClawTruncation(getToolResultText(transformed[0])); }); it("raises a structured mid-turn precheck signal after a new tool result overflows", async () => { diff --git a/src/agents/pi-embedded-runner/tool-result-context-guard.ts b/src/agents/embedded-agent-runner/tool-result-context-guard.ts similarity index 98% rename from src/agents/pi-embedded-runner/tool-result-context-guard.ts rename to src/agents/embedded-agent-runner/tool-result-context-guard.ts index 6e45144fb99..e78f9946423 100644 --- a/src/agents/pi-embedded-runner/tool-result-context-guard.ts +++ b/src/agents/embedded-agent-runner/tool-result-context-guard.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { ContextEngine, ContextEngineRuntimeContext } from "../../context-engine/types.js"; +import type { AgentMessage } from "../runtime/index.js"; import { CONTEXT_LIMIT_TRUNCATION_NOTICE, formatContextLimitTruncationNotice, @@ -372,7 +372,7 @@ export function installToolResultContextGuard(params: { ), ); - // Agent.transformContext is private in pi-coding-agent, so access it via a + // Agent.transformContext is private in session runtime, so access it via a // narrow runtime view to keep callsites type-safe while preserving behavior. const mutableAgent = params.agent as GuardableAgentRecord; const originalTransformContext = mutableAgent.transformContext; @@ -413,7 +413,7 @@ export function installToolResultContextGuard(params: { prePromptMessageCount, }) ) { - // Use the same post-truncation view Pi will send to the next model call. + // Use the same post-truncation view the runtime will send to the next model call. // Recovery re-applies truncation to the persisted session manager, so // this precheck is only a routing signal, not the source of truth. const precheck = shouldPreemptivelyCompactBeforePrompt({ diff --git a/src/agents/pi-embedded-runner/tool-result-truncation.test.ts b/src/agents/embedded-agent-runner/tool-result-truncation.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/tool-result-truncation.test.ts rename to src/agents/embedded-agent-runner/tool-result-truncation.test.ts index 8a46561169f..501ba81b81b 100644 --- a/src/agents/pi-embedded-runner/tool-result-truncation.test.ts +++ b/src/agents/embedded-agent-runner/tool-result-truncation.test.ts @@ -1,9 +1,9 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, ToolResultMessage, UserMessage } from "@earendil-works/pi-ai"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; +import type { AssistantMessage, ToolResultMessage, UserMessage } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { onSessionTranscriptUpdate } from "../../sessions/transcript-events.js"; import { makeAgentAssistantMessage } from "../test-helpers/agent-message-fixtures.js"; diff --git a/src/agents/pi-embedded-runner/tool-result-truncation.ts b/src/agents/embedded-agent-runner/tool-result-truncation.ts similarity index 98% rename from src/agents/pi-embedded-runner/tool-result-truncation.ts rename to src/agents/embedded-agent-runner/tool-result-truncation.ts index c6376d2e6c3..56405c87594 100644 --- a/src/agents/pi-embedded-runner/tool-result-truncation.ts +++ b/src/agents/embedded-agent-runner/tool-result-truncation.ts @@ -1,16 +1,16 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { TextContent } from "@earendil-works/pi-ai"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { TextContent } from "openclaw/plugin-sdk/llm"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import { resolveAgentContextLimits } from "../agent-scope.js"; +import type { AgentMessage } from "../runtime/index.js"; import { acquireSessionWriteLock, type SessionWriteLockAcquireTimeoutConfig, resolveSessionWriteLockOptions, } from "../session-write-lock.js"; +import { SessionManager } from "../sessions/index.js"; import { formatContextLimitTruncationNotice } from "./context-truncation-notice.js"; import { log } from "./logger.js"; import { @@ -33,7 +33,7 @@ const MAX_TOOL_RESULT_CONTEXT_SHARE = 0.3; /** * Low-context default cap for a single live tool result text block. * - * Pi already truncates tool results aggressively when serializing old history + * The session runtime already truncates tool results aggressively when serializing old history * for compaction summaries. For the live request path we still keep a bounded * request-local ceiling so oversized tool output cannot dominate the next turn. */ diff --git a/src/agents/pi-embedded-runner/tool-schema-runtime.test.ts b/src/agents/embedded-agent-runner/tool-schema-runtime.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/tool-schema-runtime.test.ts rename to src/agents/embedded-agent-runner/tool-schema-runtime.test.ts diff --git a/src/agents/pi-embedded-runner/tool-schema-runtime.ts b/src/agents/embedded-agent-runner/tool-schema-runtime.ts similarity index 98% rename from src/agents/pi-embedded-runner/tool-schema-runtime.ts rename to src/agents/embedded-agent-runner/tool-schema-runtime.ts index 16793dc60ef..8feb8e22bde 100644 --- a/src/agents/pi-embedded-runner/tool-schema-runtime.ts +++ b/src/agents/embedded-agent-runner/tool-schema-runtime.ts @@ -1,4 +1,3 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; import type { TSchema } from "typebox"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { ProviderRuntimePluginHandle } from "../../plugins/provider-hook-runtime.js"; @@ -8,6 +7,7 @@ import { normalizeProviderToolSchemasWithPlugin, } from "../../plugins/provider-runtime.js"; import type { ProviderToolSchemaDiagnostic } from "../../plugins/types.js"; +import type { AgentTool } from "../runtime/index.js"; import type { AnyAgentTool } from "../tools/common.js"; import { log } from "./logger.js"; diff --git a/src/agents/pi-embedded-runner/tool-split.ts b/src/agents/embedded-agent-runner/tool-split.ts similarity index 70% rename from src/agents/pi-embedded-runner/tool-split.ts rename to src/agents/embedded-agent-runner/tool-split.ts index 3233e4b8d1c..1f92431b1f6 100644 --- a/src/agents/pi-embedded-runner/tool-split.ts +++ b/src/agents/embedded-agent-runner/tool-split.ts @@ -1,6 +1,6 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; -import { toToolDefinitions } from "../pi-tool-definition-adapter.js"; -import type { HookContext } from "../pi-tools.before-tool-call.js"; +import { toToolDefinitions } from "../agent-tool-definition-adapter.js"; +import type { HookContext } from "../agent-tools.before-tool-call.js"; +import type { AgentTool } from "../runtime/index.js"; // We always pass tools via `customTools` so our policy filtering, sandbox integration, // and extended toolset remain consistent across providers. diff --git a/src/agents/pi-embedded-runner/transcript-file-state.test.ts b/src/agents/embedded-agent-runner/transcript-file-state.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/transcript-file-state.test.ts rename to src/agents/embedded-agent-runner/transcript-file-state.test.ts diff --git a/src/agents/pi-embedded-runner/transcript-file-state.ts b/src/agents/embedded-agent-runner/transcript-file-state.ts similarity index 99% rename from src/agents/pi-embedded-runner/transcript-file-state.ts rename to src/agents/embedded-agent-runner/transcript-file-state.ts index f57369a7b68..14fc0658310 100644 --- a/src/agents/pi-embedded-runner/transcript-file-state.ts +++ b/src/agents/embedded-agent-runner/transcript-file-state.ts @@ -1,6 +1,8 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; +import { appendRegularFile } from "../../infra/fs-safe.js"; +import { privateFileStore } from "../../infra/private-file-store.js"; import { buildSessionContext, CURRENT_SESSION_VERSION, @@ -10,10 +12,7 @@ import { type SessionContext, type SessionEntry, type SessionHeader, -} from "@earendil-works/pi-coding-agent"; -import { appendRegularFile } from "../../infra/fs-safe.js"; -import { privateFileStore } from "../../infra/private-file-store.js"; -import { isRecord } from "../../shared/record-coerce.js"; +} from "../sessions/index.js"; type BranchSummaryEntry = Extract; type CompactionEntry = Extract; @@ -48,6 +47,10 @@ const repairableToolCallContentTypes = new Set([ const invalidJsonlSlotType = "__openclaw_invalid_jsonl_slot"; +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + function isString(value: unknown): value is string { return typeof value === "string" && value.trim() !== ""; } diff --git a/src/agents/pi-embedded-runner/transcript-rewrite.test.ts b/src/agents/embedded-agent-runner/transcript-rewrite.test.ts similarity index 99% rename from src/agents/pi-embedded-runner/transcript-rewrite.test.ts rename to src/agents/embedded-agent-runner/transcript-rewrite.test.ts index b0a614f61bd..19a3be39587 100644 --- a/src/agents/pi-embedded-runner/transcript-rewrite.test.ts +++ b/src/agents/embedded-agent-runner/transcript-rewrite.test.ts @@ -1,8 +1,8 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { buildSessionWriteLockModuleMock } from "../../test-utils/session-write-lock-module-mock.js"; diff --git a/src/agents/pi-embedded-runner/transcript-rewrite.ts b/src/agents/embedded-agent-runner/transcript-rewrite.ts similarity index 98% rename from src/agents/pi-embedded-runner/transcript-rewrite.ts rename to src/agents/embedded-agent-runner/transcript-rewrite.ts index 88d90128e64..408ad3e3687 100644 --- a/src/agents/pi-embedded-runner/transcript-rewrite.ts +++ b/src/agents/embedded-agent-runner/transcript-rewrite.ts @@ -1,5 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; import type { TranscriptRewriteReplacement, TranscriptRewriteRequest, @@ -7,12 +5,14 @@ import type { } from "../../context-engine/types.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js"; +import type { AgentMessage } from "../runtime/index.js"; import { getRawSessionAppendMessage } from "../session-raw-append-message.js"; import { acquireSessionWriteLock, type SessionWriteLockAcquireTimeoutConfig, resolveSessionWriteLockOptions, } from "../session-write-lock.js"; +import { SessionManager } from "../sessions/index.js"; import { log } from "./logger.js"; import { persistTranscriptStateMutation, diff --git a/src/agents/pi-embedded-runner/types.ts b/src/agents/embedded-agent-runner/types.ts similarity index 96% rename from src/agents/pi-embedded-runner/types.ts rename to src/agents/embedded-agent-runner/types.ts index 50b14d4d0b6..651f68e2a03 100644 --- a/src/agents/pi-embedded-runner/types.ts +++ b/src/agents/embedded-agent-runner/types.ts @@ -6,14 +6,14 @@ import type { } from "../../config/sessions/types.js"; import type { DiagnosticTraceContext } from "../../infra/diagnostic-trace-context.js"; import type { AcceptedSessionSpawn } from "../accepted-session-spawn.js"; -import type { FallbackAttempt } from "../model-fallback.types.js"; import type { MessagingToolSend, MessagingToolSourceReplyPayload, -} from "../pi-embedded-messaging.types.js"; +} from "../embedded-agent-messaging.types.js"; +import type { FallbackAttempt } from "../model-fallback.types.js"; import type { AgentRunTimeoutPhase } from "../run-timeout-attribution.js"; -export type EmbeddedPiAgentMeta = { +export type EmbeddedAgentMeta = { sessionId: string; sessionFile?: string; provider: string; @@ -133,9 +133,9 @@ export type EmbeddedRunFailureSignal = { fatalForCron: true; }; -export type EmbeddedPiRunMeta = { +export type EmbeddedAgentRunMeta = { durationMs: number; - agentMeta?: EmbeddedPiAgentMeta; + agentMeta?: EmbeddedAgentMeta; aborted?: boolean; systemPromptReport?: SessionSystemPromptReport; finalPromptText?: string; @@ -175,7 +175,7 @@ export type EmbeddedPiRunMeta = { contextManagement?: ContextManagementTrace; }; -export type EmbeddedPiRunResult = { +export type EmbeddedAgentRunResult = { payloads?: Array<{ text?: string; mediaUrl?: string; @@ -187,7 +187,7 @@ export type EmbeddedPiRunResult = { trustedLocalMedia?: boolean; channelData?: Record; }>; - meta: EmbeddedPiRunMeta; + meta: EmbeddedAgentRunMeta; diagnosticTrace?: DiagnosticTraceContext; // True if a messaging tool successfully sent a message. // Used to suppress agent's confirmation text. @@ -210,7 +210,7 @@ export type EmbeddedPiRunResult = { successfulCronAdds?: number; }; -export type EmbeddedPiCompactResult = { +export type EmbeddedAgentCompactResult = { ok: boolean; compacted: boolean; reason?: string; diff --git a/src/agents/pi-embedded-runner/usage-accumulator.test.ts b/src/agents/embedded-agent-runner/usage-accumulator.test.ts similarity index 100% rename from src/agents/pi-embedded-runner/usage-accumulator.test.ts rename to src/agents/embedded-agent-runner/usage-accumulator.test.ts diff --git a/src/agents/pi-embedded-runner/usage-accumulator.ts b/src/agents/embedded-agent-runner/usage-accumulator.ts similarity index 100% rename from src/agents/pi-embedded-runner/usage-accumulator.ts rename to src/agents/embedded-agent-runner/usage-accumulator.ts diff --git a/src/agents/pi-embedded-runner/usage-reporting.test.ts b/src/agents/embedded-agent-runner/usage-reporting.test.ts similarity index 91% rename from src/agents/pi-embedded-runner/usage-reporting.test.ts rename to src/agents/embedded-agent-runner/usage-reporting.test.ts index be74c4a2ea0..1782ddb8039 100644 --- a/src/agents/pi-embedded-runner/usage-reporting.test.ts +++ b/src/agents/embedded-agent-runner/usage-reporting.test.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { makeAttemptResult } from "./run.overflow-compaction.fixture.js"; import { @@ -9,7 +9,7 @@ import { } from "./run.overflow-compaction.harness.js"; import type { EmbeddedRunAttemptResult } from "./run/types.js"; -let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./run.js").runEmbeddedAgent; function makeAssistantMessage( overrides: Partial = {}, @@ -35,9 +35,9 @@ function firstAttemptInput(): Record { return call[0] as Record; } -describe("runEmbeddedPiAgent usage reporting", () => { +describe("runEmbeddedAgent usage reporting", () => { beforeAll(async () => { - ({ runEmbeddedPiAgent } = await loadRunOverflowCompactionHarness()); + ({ runEmbeddedAgent } = await loadRunOverflowCompactionHarness()); }); beforeEach(() => { @@ -52,7 +52,7 @@ describe("runEmbeddedPiAgent usage reporting", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "test-session", sessionKey: "test-key", sessionFile: "/tmp/session.json", @@ -75,7 +75,7 @@ describe("runEmbeddedPiAgent usage reporting", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "test-session", sessionKey: "test-key", sessionFile: "/tmp/session.json", @@ -101,7 +101,7 @@ describe("runEmbeddedPiAgent usage reporting", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "test-session", sessionKey: "test-key", sessionFile: "/tmp/session.json", @@ -129,7 +129,7 @@ describe("runEmbeddedPiAgent usage reporting", () => { }), ); - await runEmbeddedPiAgent({ + await runEmbeddedAgent({ sessionId: "test-session", sessionKey: "test-key", sessionFile: "/tmp/session.json", @@ -172,7 +172,7 @@ describe("runEmbeddedPiAgent usage reporting", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ sessionId: "test-session", sessionKey: "test-key", sessionFile: "/tmp/session.json", @@ -193,7 +193,7 @@ describe("runEmbeddedPiAgent usage reporting", () => { expect(usage?.total).toBe(200); }); - it("reports the resolved model provider when PI marks the assistant message as pi", async () => { + it("reports the resolved model provider when OpenClaw marks the assistant message as the native runtime", async () => { mockedResolveModelAsync.mockResolvedValueOnce({ model: { id: "openai/gpt-5.4", @@ -211,15 +211,15 @@ describe("runEmbeddedPiAgent usage reporting", () => { makeAttemptResult({ assistantTexts: ["Response 1"], lastAssistant: makeAssistantMessage({ - provider: "pi", - model: "pi", + provider: "openclaw", + model: "openclaw", usage: { input: 100, output: 50, total: 150 } as unknown as AssistantMessage["usage"], }), attemptUsage: { input: 100, output: 50, total: 150 }, }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ sessionId: "test-session", sessionKey: "test-key", sessionFile: "/tmp/session.json", diff --git a/src/agents/pi-embedded-runner/utils.ts b/src/agents/embedded-agent-runner/utils.ts similarity index 78% rename from src/agents/pi-embedded-runner/utils.ts rename to src/agents/embedded-agent-runner/utils.ts index bf0b44362da..3f032a76ef2 100644 --- a/src/agents/pi-embedded-runner/utils.ts +++ b/src/agents/embedded-agent-runner/utils.ts @@ -1,5 +1,5 @@ -import type { ThinkingLevel } from "@earendil-works/pi-agent-core"; import type { ReasoningLevel, ThinkLevel } from "../../auto-reply/thinking.js"; +import type { ThinkingLevel } from "../runtime/index.js"; export function normalizeContextTokenBudget(value: unknown): number | undefined { return typeof value === "number" && Number.isFinite(value) && value > 0 @@ -8,14 +8,14 @@ export function normalizeContextTokenBudget(value: unknown): number | undefined } export function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel { - // pi-agent-core supports "xhigh"; OpenClaw enables it for specific models. + // agent runtime supports "xhigh"; OpenClaw enables it for specific models. if (!level) { return "off"; } if (level === "max") { return "xhigh"; } - // "adaptive" maps to "medium" at the pi-agent-core layer. The Pi SDK + // "adaptive" maps to "medium" at the agent runtime layer. The provider adapter // provider then translates this to `thinking.type: "adaptive"` with // `output_config.effort: "medium"` for models that support it (Opus 4.6, // Sonnet 4.6). diff --git a/src/agents/pi-embedded-runner/wait-for-idle-before-flush.ts b/src/agents/embedded-agent-runner/wait-for-idle-before-flush.ts similarity index 100% rename from src/agents/pi-embedded-runner/wait-for-idle-before-flush.ts rename to src/agents/embedded-agent-runner/wait-for-idle-before-flush.ts diff --git a/src/agents/pi-embedded-subscribe.block-reply-rejections.test.ts b/src/agents/embedded-agent-subscribe.block-reply-rejections.test.ts similarity index 93% rename from src/agents/pi-embedded-subscribe.block-reply-rejections.test.ts rename to src/agents/embedded-agent-subscribe.block-reply-rejections.test.ts index 79fbd250f60..43fcde12e8b 100644 --- a/src/agents/pi-embedded-subscribe.block-reply-rejections.test.ts +++ b/src/agents/embedded-agent-subscribe.block-reply-rejections.test.ts @@ -4,14 +4,14 @@ import { emitAssistantTextDelta, emitAssistantTextEnd, emitMessageStartAndEndForAssistantText, -} from "./pi-embedded-subscribe.e2e-harness.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; const waitForAsyncCallbacks = async () => { await Promise.resolve(); await new Promise((resolve) => setImmediate(resolve)); }; -describe("subscribeEmbeddedPiSession block reply rejections", () => { +describe("subscribeEmbeddedAgentSession block reply rejections", () => { const unhandledRejections: unknown[] = []; const onUnhandledRejection = (reason: unknown) => { unhandledRejections.push(reason); diff --git a/src/agents/pi-embedded-subscribe.code-span-awareness.test.ts b/src/agents/embedded-agent-subscribe.code-span-awareness.test.ts similarity index 89% rename from src/agents/pi-embedded-subscribe.code-span-awareness.test.ts rename to src/agents/embedded-agent-subscribe.code-span-awareness.test.ts index 4efa112161b..589d264155d 100644 --- a/src/agents/pi-embedded-subscribe.code-span-awareness.test.ts +++ b/src/agents/embedded-agent-subscribe.code-span-awareness.test.ts @@ -2,15 +2,15 @@ import { describe, expect, it, vi } from "vitest"; import { createStubSessionHarness, emitAssistantTextDelta, -} from "./pi-embedded-subscribe.e2e-harness.js"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; -describe("subscribeEmbeddedPiSession thinking tag code span awareness", () => { +describe("subscribeEmbeddedAgentSession thinking tag code span awareness", () => { function createPartialReplyHarness() { const { session, emit } = createStubSessionHarness(); const onPartialReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onPartialReply, diff --git a/src/agents/pi-embedded-subscribe.compaction-test-helpers.ts b/src/agents/embedded-agent-subscribe.compaction-test-helpers.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.compaction-test-helpers.ts rename to src/agents/embedded-agent-subscribe.compaction-test-helpers.ts diff --git a/src/agents/pi-embedded-subscribe.e2e-harness.ts b/src/agents/embedded-agent-subscribe.e2e-harness.ts similarity index 86% rename from src/agents/pi-embedded-subscribe.e2e-harness.ts rename to src/agents/embedded-agent-subscribe.e2e-harness.ts index 0b522f6754c..0d0b940d968 100644 --- a/src/agents/pi-embedded-subscribe.e2e-harness.ts +++ b/src/agents/embedded-agent-subscribe.e2e-harness.ts @@ -1,12 +1,12 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { expect } from "vitest"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; -type SubscribeEmbeddedPiSession = typeof subscribeEmbeddedPiSession; -type SubscribeEmbeddedPiSessionParams = Parameters[0]; -type PiSession = Parameters[0]["session"]; -type OnBlockReply = NonNullable; -type BlockReplyChunking = NonNullable; +type SubscribeEmbeddedAgentSession = typeof subscribeEmbeddedAgentSession; +type SubscribeEmbeddedAgentSessionParams = Parameters[0]; +type EmbeddedAgentSession = Parameters[0]["session"]; +type OnBlockReply = NonNullable; +type BlockReplyChunking = NonNullable; export const THINKING_TAG_CASES = [ { tag: "think", open: "", close: "" }, @@ -17,7 +17,7 @@ export const THINKING_TAG_CASES = [ ] as const; export function createStubSessionHarness(): { - session: PiSession; + session: EmbeddedAgentSession; emit: (evt: unknown) => void; } { let handler: ((evt: unknown) => void) | undefined; @@ -26,24 +26,24 @@ export function createStubSessionHarness(): { handler = fn; return () => {}; }, - } as unknown as PiSession; + } as unknown as EmbeddedAgentSession; return { session, emit: (evt: unknown) => handler?.(evt) }; } export function createSubscribedSessionHarness( - params: Omit[0], "session"> & { - sessionExtras?: Partial; + params: Omit[0], "session"> & { + sessionExtras?: Partial; }, ): { emit: (evt: unknown) => void; - session: PiSession; - subscription: ReturnType; + session: EmbeddedAgentSession; + subscription: ReturnType; } { const { sessionExtras, ...subscribeParams } = params; const { session, emit } = createStubSessionHarness(); const mergedSession = Object.assign(session, sessionExtras ?? {}); - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ ...subscribeParams, trustedLocalMediaToolNames: subscribeParams.trustedLocalMediaToolNames ?? subscribeParams.builtinToolNames, @@ -59,7 +59,7 @@ export function createParagraphChunkedBlockReplyHarness(params: { }): { emit: (evt: unknown) => void; onBlockReply: OnBlockReply; - subscription: ReturnType; + subscription: ReturnType; } { const onBlockReply: OnBlockReply = params.onBlockReply ?? (() => {}); const { emit, subscription } = createSubscribedSessionHarness({ @@ -81,7 +81,7 @@ export function createTextEndBlockReplyHarness(params?: { }): { emit: (evt: unknown) => void; onBlockReply: OnBlockReply; - subscription: ReturnType; + subscription: ReturnType; } { const onBlockReply: OnBlockReply = params?.onBlockReply ?? (() => {}); const { emit, subscription } = createSubscribedSessionHarness({ diff --git a/src/agents/pi-embedded-subscribe.handlers.compaction.runtime.ts b/src/agents/embedded-agent-subscribe.handlers.compaction.runtime.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.handlers.compaction.runtime.ts rename to src/agents/embedded-agent-subscribe.handlers.compaction.runtime.ts diff --git a/src/agents/pi-embedded-subscribe.handlers.compaction.test.ts b/src/agents/embedded-agent-subscribe.handlers.compaction.test.ts similarity index 96% rename from src/agents/pi-embedded-subscribe.handlers.compaction.test.ts rename to src/agents/embedded-agent-subscribe.handlers.compaction.test.ts index b0cdb62d04c..42d28d0563b 100644 --- a/src/agents/pi-embedded-subscribe.handlers.compaction.test.ts +++ b/src/agents/embedded-agent-subscribe.handlers.compaction.test.ts @@ -7,13 +7,13 @@ import { readCompactionCount, seedSessionStore, waitForCompactionCount, -} from "./pi-embedded-subscribe.compaction-test-helpers.js"; +} from "./embedded-agent-subscribe.compaction-test-helpers.js"; import { handleCompactionEnd, handleCompactionStart, reconcileSessionStoreCompactionCountAfterSuccess, -} from "./pi-embedded-subscribe.handlers.compaction.js"; -import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js"; +} from "./embedded-agent-subscribe.handlers.compaction.js"; +import type { EmbeddedAgentSubscribeContext } from "./embedded-agent-subscribe.handlers.types.js"; function createCompactionContext(params: { storePath: string; @@ -21,7 +21,7 @@ function createCompactionContext(params: { agentId?: string; initialCount: number; info?: (message: string, meta?: Record) => void; -}): EmbeddedPiSubscribeContext { +}): EmbeddedAgentSubscribeContext { let compactionCount = params.initialCount; return { params: { @@ -53,7 +53,7 @@ function createCompactionContext(params: { getCompactionCount: () => compactionCount, noteCompactionTokensAfter: vi.fn(), getLastCompactionTokensAfter: vi.fn(() => undefined), - } as unknown as EmbeddedPiSubscribeContext; + } as unknown as EmbeddedAgentSubscribeContext; } function loggedInfoMetaAt(info: ReturnType, index: number): Record { diff --git a/src/agents/pi-embedded-subscribe.handlers.compaction.ts b/src/agents/embedded-agent-subscribe.handlers.compaction.ts similarity index 92% rename from src/agents/pi-embedded-subscribe.handlers.compaction.ts rename to src/agents/embedded-agent-subscribe.handlers.compaction.ts index 0daa5fbf75e..c7398c6278b 100644 --- a/src/agents/pi-embedded-subscribe.handlers.compaction.ts +++ b/src/agents/embedded-agent-subscribe.handlers.compaction.ts @@ -1,7 +1,7 @@ -import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent"; import { emitAgentEvent } from "../infra/agent-events.js"; import { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; -import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js"; +import type { EmbeddedAgentSubscribeContext } from "./embedded-agent-subscribe.handlers.types.js"; +import type { AgentSessionEvent } from "./sessions/index.js"; import { makeZeroUsageSnapshot } from "./usage.js"; type SessionCompactionStartEvent = Extract; @@ -35,7 +35,10 @@ function compactionLogKind(reason: CompactionReason): string { return reason === "manual" ? "manual compaction" : "auto-compaction"; } -export function handleCompactionStart(ctx: EmbeddedPiSubscribeContext, evt: CompactionStartEvent) { +export function handleCompactionStart( + ctx: EmbeddedAgentSubscribeContext, + evt: CompactionStartEvent, +) { const reason = normalizeCompactionReason(evt.reason); const kind = compactionLogKind(reason); ctx.state.compactionInFlight = true; @@ -77,7 +80,7 @@ export function handleCompactionStart(ctx: EmbeddedPiSubscribeContext, evt: Comp } } -export function handleCompactionEnd(ctx: EmbeddedPiSubscribeContext, evt: CompactionEndEvent) { +export function handleCompactionEnd(ctx: EmbeddedAgentSubscribeContext, evt: CompactionEndEvent) { const reason = normalizeCompactionReason(evt.reason); const kind = compactionLogKind(reason); ctx.state.compactionInFlight = false; @@ -174,11 +177,11 @@ export async function reconcileSessionStoreCompactionCountAfterSuccess(params: { now?: number; }): Promise { const { reconcileSessionStoreCompactionCountAfterSuccess: reconcile } = - await import("./pi-embedded-subscribe.handlers.compaction.runtime.js"); + await import("./embedded-agent-subscribe.handlers.compaction.runtime.js"); return reconcile(params); } -function clearStaleAssistantUsageOnSessionMessages(ctx: EmbeddedPiSubscribeContext): void { +function clearStaleAssistantUsageOnSessionMessages(ctx: EmbeddedAgentSubscribeContext): void { const messages = ctx.params.session.messages; if (!Array.isArray(messages)) { return; @@ -191,7 +194,7 @@ function clearStaleAssistantUsageOnSessionMessages(ctx: EmbeddedPiSubscribeConte if (candidate.role !== "assistant") { continue; } - // pi-coding-agent expects assistant usage to exist when computing context usage. + // session runtime expects assistant usage to exist when computing context usage. // Reset stale snapshots to zeros instead of deleting the field. candidate.usage = makeZeroUsageSnapshot(); } diff --git a/src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts b/src/agents/embedded-agent-subscribe.handlers.lifecycle.test.ts similarity index 97% rename from src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts rename to src/agents/embedded-agent-subscribe.handlers.lifecycle.test.ts index f145a78322b..d6a1d55920a 100644 --- a/src/agents/pi-embedded-subscribe.handlers.lifecycle.test.ts +++ b/src/agents/embedded-agent-subscribe.handlers.lifecycle.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import { createInlineCodeState } from "../markdown/code-spans.js"; -import { handleAgentEnd } from "./pi-embedded-subscribe.handlers.lifecycle.js"; -import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js"; +import { handleAgentEnd } from "./embedded-agent-subscribe.handlers.lifecycle.js"; +import type { EmbeddedAgentSubscribeContext } from "./embedded-agent-subscribe.handlers.types.js"; const { emitAgentEventMock } = vi.hoisted(() => ({ emitAgentEventMock: vi.fn(), @@ -19,7 +19,7 @@ function createContext( onBlockReply?: ((payload: unknown) => void) | undefined; onBlockReplyFlush?: () => void | Promise; }, -): EmbeddedPiSubscribeContext { +): EmbeddedAgentSubscribeContext { const hasOnBlockReplyOverride = Boolean(overrides && "onBlockReply" in overrides); const onBlockReply = hasOnBlockReplyOverride ? overrides?.onBlockReply : vi.fn(); const emitBlockReply = vi.fn(); @@ -34,7 +34,7 @@ function createContext( onBlockReplyFlush: overrides?.onBlockReplyFlush, }, state: { - lastAssistant: lastAssistant as EmbeddedPiSubscribeContext["state"]["lastAssistant"], + lastAssistant: lastAssistant as EmbeddedAgentSubscribeContext["state"]["lastAssistant"], pendingCompactionRetry: 0, pendingToolMediaUrls: [], pendingToolAudioAsVoice: false, @@ -53,10 +53,10 @@ function createContext( emitBlockReply, resolveCompactionRetry: vi.fn(), maybeResolveCompactionWait: vi.fn(), - } as unknown as EmbeddedPiSubscribeContext; + } as unknown as EmbeddedAgentSubscribeContext; } -async function handleAgentEndAndReadWarnMeta(ctx: EmbeddedPiSubscribeContext) { +async function handleAgentEndAndReadWarnMeta(ctx: EmbeddedAgentSubscribeContext) { await handleAgentEnd(ctx); const warn = vi.mocked(ctx.log.warn); @@ -81,7 +81,7 @@ function firstMockCall(mock: { mock: { calls: ReadonlyArray { +function firstWarnMeta(ctx: EmbeddedAgentSubscribeContext): Record { return readRecord(firstMockCall(vi.mocked(ctx.log.warn))[1]); } diff --git a/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts b/src/agents/embedded-agent-subscribe.handlers.lifecycle.ts similarity index 92% rename from src/agents/pi-embedded-subscribe.handlers.lifecycle.ts rename to src/agents/embedded-agent-subscribe.handlers.lifecycle.ts index 36bfabcbcf1..24ea909fec4 100644 --- a/src/agents/pi-embedded-subscribe.handlers.lifecycle.ts +++ b/src/agents/embedded-agent-subscribe.handlers.lifecycle.ts @@ -6,24 +6,24 @@ import { buildTextObservationFields, sanitizeForConsole, shouldSuppressRawErrorConsoleSuffix, -} from "./pi-embedded-error-observation.js"; -import { classifyFailoverReason, formatAssistantErrorText } from "./pi-embedded-helpers.js"; -import { hasCommittedMessagingToolDeliveryEvidence } from "./pi-embedded-runner/delivery-evidence.js"; -import { isIncompleteTerminalAssistantTurn } from "./pi-embedded-runner/run/incomplete-turn.js"; +} from "./embedded-agent-error-observation.js"; +import { classifyFailoverReason, formatAssistantErrorText } from "./embedded-agent-helpers.js"; +import { hasCommittedMessagingToolDeliveryEvidence } from "./embedded-agent-runner/delivery-evidence.js"; +import { isIncompleteTerminalAssistantTurn } from "./embedded-agent-runner/run/incomplete-turn.js"; import { consumePendingToolMediaReply, hasAssistantVisibleReply, -} from "./pi-embedded-subscribe.handlers.messages.js"; -import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js"; -import { isPromiseLike } from "./pi-embedded-subscribe.promise.js"; -import { isAssistantMessage } from "./pi-embedded-utils.js"; +} from "./embedded-agent-subscribe.handlers.messages.js"; +import type { EmbeddedAgentSubscribeContext } from "./embedded-agent-subscribe.handlers.types.js"; +import { isPromiseLike } from "./embedded-agent-subscribe.promise.js"; +import { isAssistantMessage } from "./embedded-agent-utils.js"; export { handleCompactionEnd, handleCompactionStart, -} from "./pi-embedded-subscribe.handlers.compaction.js"; +} from "./embedded-agent-subscribe.handlers.compaction.js"; -export function handleAgentStart(ctx: EmbeddedPiSubscribeContext) { +export function handleAgentStart(ctx: EmbeddedAgentSubscribeContext) { ctx.log.debug(`embedded run agent start: runId=${ctx.params.runId}`); emitAgentEvent({ runId: ctx.params.runId, @@ -39,7 +39,7 @@ export function handleAgentStart(ctx: EmbeddedPiSubscribeContext) { }); } -export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext): void | Promise { +export function handleAgentEnd(ctx: EmbeddedAgentSubscribeContext): void | Promise { const lastAssistant = ctx.state.lastAssistant; const isError = isAssistantMessage(lastAssistant) && lastAssistant.stopReason === "error"; let lifecycleErrorText: string | undefined; diff --git a/src/agents/pi-embedded-subscribe.handlers.messages.test.ts b/src/agents/embedded-agent-subscribe.handlers.messages.test.ts similarity index 98% rename from src/agents/pi-embedded-subscribe.handlers.messages.test.ts rename to src/agents/embedded-agent-subscribe.handlers.messages.test.ts index 422a9a4b478..2ae510f92e8 100644 --- a/src/agents/pi-embedded-subscribe.handlers.messages.test.ts +++ b/src/agents/embedded-agent-subscribe.handlers.messages.test.ts @@ -12,13 +12,13 @@ import { readPendingToolMediaReply, recordPendingAssistantReplyDirectives, resolveSilentReplyFallbackText, -} from "./pi-embedded-subscribe.handlers.messages.js"; -import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js"; +} from "./embedded-agent-subscribe.handlers.messages.js"; +import type { EmbeddedAgentSubscribeContext } from "./embedded-agent-subscribe.handlers.types.js"; import { createOpenAiResponsesPartial, createOpenAiResponsesTextBlock, createOpenAiResponsesTextEvent as createTextUpdateEvent, -} from "./pi-embedded-subscribe.openai-responses.test-helpers.js"; +} from "./embedded-agent-subscribe.openai-responses.test-helpers.js"; function createMessageUpdateContext( params: { @@ -71,7 +71,7 @@ function createMessageUpdateContext( resetAssistantMessageState: params.resetAssistantMessageState ?? vi.fn(), recordAssistantUsage: vi.fn(), commitAssistantUsage: vi.fn(), - } as unknown as EmbeddedPiSubscribeContext; + } as unknown as EmbeddedAgentSubscribeContext; } function createMessageEndContext( @@ -138,7 +138,7 @@ function createMessageEndContext( emitReasoningStream: vi.fn(), flushBlockReplyBuffer: vi.fn(), blockChunker: null, - } as unknown as EmbeddedPiSubscribeContext; + } as unknown as EmbeddedAgentSubscribeContext; } function firstMockCall(mock: { mock: { calls: unknown[][] } }, label: string): unknown[] { diff --git a/src/agents/pi-embedded-subscribe.handlers.messages.ts b/src/agents/embedded-agent-subscribe.handlers.messages.ts similarity index 95% rename from src/agents/pi-embedded-subscribe.handlers.messages.ts rename to src/agents/embedded-agent-subscribe.handlers.messages.ts index 3d5ee9d9dc1..a8a7baf2d3a 100644 --- a/src/agents/pi-embedded-subscribe.handlers.messages.ts +++ b/src/agents/embedded-agent-subscribe.handlers.messages.ts @@ -1,5 +1,4 @@ -import type { AgentEvent, AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload"; import { parseReplyDirectives, @@ -20,15 +19,15 @@ import { uniqueStrings } from "../shared/string-normalization.js"; import { isMessagingToolDuplicateNormalized, normalizeTextForComparison, -} from "./pi-embedded-helpers.js"; -import type { BlockReplyPayload } from "./pi-embedded-payloads.js"; +} from "./embedded-agent-helpers.js"; +import type { BlockReplyPayload } from "./embedded-agent-payloads.js"; import type { - EmbeddedPiSubscribeContext, - EmbeddedPiSubscribeState, -} from "./pi-embedded-subscribe.handlers.types.js"; -import { isPromiseLike } from "./pi-embedded-subscribe.promise.js"; -import { appendRawStream } from "./pi-embedded-subscribe.raw-stream.js"; -import { warnIfAssistantEmittedToolText } from "./pi-embedded-subscribe.tool-text-diagnostics.js"; + EmbeddedAgentSubscribeContext, + EmbeddedAgentSubscribeState, +} from "./embedded-agent-subscribe.handlers.types.js"; +import { isPromiseLike } from "./embedded-agent-subscribe.promise.js"; +import { appendRawStream } from "./embedded-agent-subscribe.raw-stream.js"; +import { warnIfAssistantEmittedToolText } from "./embedded-agent-subscribe.tool-text-diagnostics.js"; import { extractAssistantText, extractAssistantThinking, @@ -36,7 +35,8 @@ import { extractThinkingFromTaggedStream, extractThinkingFromTaggedText, promoteThinkingTagsToBlocks, -} from "./pi-embedded-utils.js"; +} from "./embedded-agent-utils.js"; +import type { AgentEvent, AgentMessage } from "./runtime/index.js"; function shouldSuppressAssistantVisibleOutput(message: AgentMessage | undefined): boolean { return resolveAssistantMessagePhase(message) === "commentary"; @@ -132,7 +132,7 @@ function resolveAssistantStreamItemId(params: { return undefined; } -function emitReasoningEnd(ctx: EmbeddedPiSubscribeContext) { +function emitReasoningEnd(ctx: EmbeddedAgentSubscribeContext) { if (!ctx.state.reasoningStreamOpen) { return; } @@ -140,20 +140,20 @@ function emitReasoningEnd(ctx: EmbeddedPiSubscribeContext) { void ctx.params.onReasoningEnd?.(); } -function openReasoningStream(ctx: EmbeddedPiSubscribeContext) { +function openReasoningStream(ctx: EmbeddedAgentSubscribeContext) { ctx.state.reasoningStreamOpen = true; } function shouldSuppressDeterministicApprovalOutput( state: Pick< - EmbeddedPiSubscribeState, + EmbeddedAgentSubscribeState, "deterministicApprovalPromptPending" | "deterministicApprovalPromptSent" >, ): boolean { return state.deterministicApprovalPromptPending || state.deterministicApprovalPromptSent; } -function appendBlockReplyChunk(ctx: EmbeddedPiSubscribeContext, chunk: string) { +function appendBlockReplyChunk(ctx: EmbeddedAgentSubscribeContext, chunk: string) { if (ctx.blockChunker) { ctx.blockChunker.append(chunk); return; @@ -161,7 +161,7 @@ function appendBlockReplyChunk(ctx: EmbeddedPiSubscribeContext, chunk: string) { ctx.state.blockBuffer += chunk; } -function replaceBlockReplyBuffer(ctx: EmbeddedPiSubscribeContext, text: string) { +function replaceBlockReplyBuffer(ctx: EmbeddedAgentSubscribeContext, text: string) { if (ctx.blockChunker) { ctx.blockChunker.reset(); ctx.blockChunker.append(text); @@ -218,7 +218,7 @@ export function resolveSilentReplyFallbackText(params: { function clearPendingToolMedia( state: Pick< - EmbeddedPiSubscribeState, + EmbeddedAgentSubscribeState, "pendingToolMediaUrls" | "pendingToolAudioAsVoice" | "pendingToolTrustedLocalMedia" >, ) { @@ -233,7 +233,7 @@ function hasReplyMedia(payload: BlockReplyPayload): boolean { export function consumePendingToolMediaIntoReply( state: Pick< - EmbeddedPiSubscribeState, + EmbeddedAgentSubscribeState, "pendingToolMediaUrls" | "pendingToolAudioAsVoice" | "pendingToolTrustedLocalMedia" >, payload: BlockReplyPayload, @@ -269,7 +269,7 @@ export function consumePendingToolMediaIntoReply( export function consumePendingToolMediaReply( state: Pick< - EmbeddedPiSubscribeState, + EmbeddedAgentSubscribeState, "pendingToolMediaUrls" | "pendingToolAudioAsVoice" | "pendingToolTrustedLocalMedia" >, ): BlockReplyPayload | null { @@ -283,7 +283,7 @@ export function consumePendingToolMediaReply( export function readPendingToolMediaReply( state: Pick< - EmbeddedPiSubscribeState, + EmbeddedAgentSubscribeState, "pendingToolMediaUrls" | "pendingToolAudioAsVoice" | "pendingToolTrustedLocalMedia" >, ): BlockReplyPayload | null { @@ -344,7 +344,7 @@ function mergeReplyDirectiveResults( } export function recordPendingAssistantReplyDirectives( - state: Pick, + state: Pick, parsed: ReplyDirectiveParseResult | null | undefined, ) { if (!hasReplyDirectiveMetadataResult(parsed)) { @@ -364,7 +364,7 @@ export function recordPendingAssistantReplyDirectives( } export function consumePendingAssistantReplyDirectivesIntoReply( - state: Pick, + state: Pick, payload: BlockReplyPayload, ): BlockReplyPayload { if (payload.isReasoning || !state.pendingAssistantReplyDirectives) { @@ -419,7 +419,7 @@ export function buildAssistantStreamData(params: { } export function handleMessageStart( - ctx: EmbeddedPiSubscribeContext, + ctx: EmbeddedAgentSubscribeContext, evt: AgentEvent & { message: AgentMessage }, ) { const msg = evt.message; @@ -438,7 +438,7 @@ export function handleMessageStart( } export function handleMessageUpdate( - ctx: EmbeddedPiSubscribeContext, + ctx: EmbeddedAgentSubscribeContext, evt: AgentEvent & { message: AgentMessage; assistantMessageEvent?: unknown }, ) { const msg = evt.message; @@ -693,7 +693,7 @@ export function handleMessageUpdate( } export function handleMessageEnd( - ctx: EmbeddedPiSubscribeContext, + ctx: EmbeddedAgentSubscribeContext, evt: AgentEvent & { message: AgentMessage }, ): void | Promise { const msg = evt.message; diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.media.test.ts b/src/agents/embedded-agent-subscribe.handlers.tools.media.test.ts similarity index 86% rename from src/agents/pi-embedded-subscribe.handlers.tools.media.test.ts rename to src/agents/embedded-agent-subscribe.handlers.tools.media.test.ts index e78ec14b2e4..8e27f09b873 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.media.test.ts +++ b/src/agents/embedded-agent-subscribe.handlers.tools.media.test.ts @@ -2,8 +2,8 @@ import { describe, expect, it, vi } from "vitest"; import { handleToolExecutionEnd, handleToolExecutionStart, -} from "./pi-embedded-subscribe.handlers.tools.js"; -import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js"; +} from "./embedded-agent-subscribe.handlers.tools.js"; +import type { EmbeddedAgentSubscribeContext } from "./embedded-agent-subscribe.handlers.types.js"; // Minimal mock context factory. Only the fields needed for the media emission path. function createMockContext(overrides?: { @@ -12,7 +12,7 @@ function createMockContext(overrides?: { toolResultFormat?: "markdown" | "plain"; builtinToolNames?: ReadonlySet; trustedLocalMediaToolNames?: ReadonlySet; -}): EmbeddedPiSubscribeContext { +}): EmbeddedAgentSubscribeContext { const onToolResult = overrides?.onToolResult ?? vi.fn(); return { params: { @@ -40,13 +40,10 @@ function createMockContext(overrides?: { messagingToolSentTargets: [], deterministicApprovalPromptPending: false, deterministicApprovalPromptSent: false, - hadDeterministicSideEffect: false, - replayState: { replayInvalid: false, hadPotentialSideEffects: false }, }, log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn() }, builtinToolNames: overrides?.builtinToolNames, - trustedLocalMediaToolNames: - overrides?.trustedLocalMediaToolNames ?? overrides?.builtinToolNames, + trustedLocalMediaToolNames: overrides?.trustedLocalMediaToolNames, shouldEmitToolResult: vi.fn(() => false), shouldEmitToolOutput: vi.fn(() => overrides?.shouldEmitToolOutput ?? false), emitToolSummary: vi.fn(), @@ -74,10 +71,10 @@ function createMockContext(overrides?: { incrementCompactionCount: vi.fn(), getUsageTotals: vi.fn(() => undefined), getCompactionCount: vi.fn(() => 0), - } as unknown as EmbeddedPiSubscribeContext; + } as unknown as EmbeddedAgentSubscribeContext; } -function firstEmitToolOutputCall(ctx: EmbeddedPiSubscribeContext) { +function firstEmitToolOutputCall(ctx: EmbeddedAgentSubscribeContext) { expect(ctx.emitToolOutput).toHaveBeenCalledTimes(1); const call = vi.mocked(ctx.emitToolOutput).mock.calls[0]; if (!call) { @@ -87,7 +84,7 @@ function firstEmitToolOutputCall(ctx: EmbeddedPiSubscribeContext) { } async function emitPngMediaToolResult( - ctx: EmbeddedPiSubscribeContext, + ctx: EmbeddedAgentSubscribeContext, opts?: { isError?: boolean }, ) { await handleToolExecutionEnd(ctx, { @@ -106,7 +103,7 @@ async function emitPngMediaToolResult( } async function emitUntrustedToolMediaResult( - ctx: EmbeddedPiSubscribeContext, + ctx: EmbeddedAgentSubscribeContext, mediaPathOrUrl: string, ) { await handleToolExecutionEnd(ctx, { @@ -120,7 +117,7 @@ async function emitUntrustedToolMediaResult( }); } -async function emitMcpMediaToolResult(ctx: EmbeddedPiSubscribeContext, mediaPathOrUrl: string) { +async function emitMcpMediaToolResult(ctx: EmbeddedAgentSubscribeContext, mediaPathOrUrl: string) { await handleToolExecutionEnd(ctx, { type: "tool_execution_end", toolName: "browser", @@ -498,7 +495,6 @@ describe("handleToolExecutionEnd media emission", () => { expect(ctx.emitToolOutput).toHaveBeenCalledTimes(1); expect(ctx.state.pendingToolMediaUrls).toStrictEqual([]); }); - it("queues structured media once for markdown verbose output", async () => { const ctx = await handleVerboseGeneratedImage("markdown"); @@ -536,48 +532,6 @@ describe("handleToolExecutionEnd media emission", () => { }, ); - it("marks async-started media generation in tool metadata", async () => { - const ctx = createMockContext({ shouldEmitToolOutput: false, onToolResult: vi.fn() }); - - await handleToolExecutionStart(ctx, { - type: "tool_execution_start", - toolName: "image_generate", - toolCallId: "tc-1", - args: { action: "generate", prompt: "a portrait" }, - }); - await handleToolExecutionEnd(ctx, { - type: "tool_execution_end", - toolName: "image_generate", - toolCallId: "tc-1", - isError: false, - result: { - content: [ - { - type: "text", - text: "Background task started for image generation (task-123).", - }, - ], - details: { - async: true, - status: "started", - taskId: "task-123", - }, - }, - }); - - expect(ctx.state.toolMetas).toEqual([ - expect.objectContaining({ - toolName: "image_generate", - asyncStarted: true, - }), - ]); - expect(ctx.state.hadDeterministicSideEffect).toBe(true); - expect(ctx.state.replayState).toEqual({ - replayInvalid: true, - hadPotentialSideEffects: true, - }); - }); - it("does NOT emit media for error results", async () => { const onToolResult = vi.fn(); const ctx = createMockContext({ shouldEmitToolOutput: false, onToolResult }); @@ -698,61 +652,6 @@ describe("handleToolExecutionEnd media emission", () => { expect(ctx.state.pendingToolTrustedLocalMedia).toBe(true); }); - it("does NOT queue structured media marked as non-outbound", async () => { - const ctx = createMockContext({ - shouldEmitToolOutput: false, - onToolResult: vi.fn(), - builtinToolNames: new Set(["message"]), - }); - - await handleToolExecutionEnd(ctx, { - type: "tool_execution_end", - toolName: "message", - toolCallId: "tc-1", - isError: false, - result: { - content: [{ type: "text", text: "Downloaded Slack file to /tmp/report.pdf" }], - details: { - media: { - mediaUrl: "/tmp/report.pdf", - outbound: false, - }, - }, - }, - }); - - expect(ctx.state.pendingToolMediaUrls).toStrictEqual([]); - }); - - it("does NOT queue image fallback paths when media is marked as non-outbound", async () => { - const ctx = createMockContext({ - shouldEmitToolOutput: false, - onToolResult: vi.fn(), - builtinToolNames: new Set(["message"]), - }); - - await handleToolExecutionEnd(ctx, { - type: "tool_execution_end", - toolName: "message", - toolCallId: "tc-1", - isError: false, - result: { - content: [ - { type: "text", text: "Downloaded Slack image" }, - { type: "image", data: "base64", mimeType: "image/png" }, - ], - details: { - path: "/tmp/slack-image.png", - media: { - outbound: false, - }, - }, - }, - }); - - expect(ctx.state.pendingToolMediaUrls).toStrictEqual([]); - }); - it("queues trusted TTS local media when the exact built-in name is absent", async () => { const ctx = createMockContext({ shouldEmitToolOutput: false, diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.test.ts b/src/agents/embedded-agent-subscribe.handlers.tools.test.ts similarity index 99% rename from src/agents/pi-embedded-subscribe.handlers.tools.test.ts rename to src/agents/embedded-agent-subscribe.handlers.tools.test.ts index 1c80060a0c7..0540331c2f0 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.test.ts +++ b/src/agents/embedded-agent-subscribe.handlers.tools.test.ts @@ -1,19 +1,19 @@ -import type { AgentEvent } from "@earendil-works/pi-agent-core"; +import type { AgentEvent } from "openclaw/plugin-sdk/agent-core"; import { afterEach, describe, expect, it, vi } from "vitest"; import { onAgentEvent as registerAgentEventListener, resetAgentEventsForTest, } from "../infra/agent-events.js"; -import type { MessagingToolSend } from "./pi-embedded-messaging.types.js"; +import type { MessagingToolSend } from "./embedded-agent-messaging.types.js"; import { handleToolExecutionEnd, handleToolExecutionStart, handleToolExecutionUpdate, -} from "./pi-embedded-subscribe.handlers.tools.js"; +} from "./embedded-agent-subscribe.handlers.tools.js"; import type { ToolCallSummary, ToolHandlerContext, -} from "./pi-embedded-subscribe.handlers.types.js"; +} from "./embedded-agent-subscribe.handlers.types.js"; type ToolExecutionStartEvent = Extract; type ToolExecutionEndEvent = Extract; @@ -228,7 +228,7 @@ describe("handleToolExecutionStart read path checks", () => { phase: "tool_execution_started", tool: "read", toolCallId: "tool-1", - source: "pi-embedded", + source: "embedded-agent", }); expect(warn).not.toHaveBeenCalled(); expect(trace).toHaveBeenCalledTimes(1); diff --git a/src/agents/pi-embedded-subscribe.handlers.tools.ts b/src/agents/embedded-agent-subscribe.handlers.tools.ts similarity index 98% rename from src/agents/pi-embedded-subscribe.handlers.tools.ts rename to src/agents/embedded-agent-subscribe.handlers.tools.ts index 4f2c654bcef..dce939c47e0 100644 --- a/src/agents/pi-embedded-subscribe.handlers.tools.ts +++ b/src/agents/embedded-agent-subscribe.handlers.tools.ts @@ -1,4 +1,3 @@ -import type { AgentEvent } from "@earendil-works/pi-agent-core"; import { HEARTBEAT_RESPONSE_TOOL_NAME, normalizeHeartbeatToolResponse, @@ -30,16 +29,15 @@ import { normalizeAcceptedSessionSpawnResult } from "./accepted-session-spawn.js import type { ApplyPatchSummary } from "./apply-patch.js"; import type { ExecToolDetails } from "./bash-tools.exec-types.js"; import { sanitizeForConsole } from "./console-sanitize.js"; -import { parseExecApprovalResultText } from "./exec-approval-result.js"; -import { normalizeTextForComparison } from "./pi-embedded-helpers.js"; -import { isMessagingTool, isMessagingToolSendAction } from "./pi-embedded-messaging.js"; -import type { MessagingToolSourceReplyPayload } from "./pi-embedded-messaging.types.js"; -import { mergeEmbeddedRunReplayState } from "./pi-embedded-runner/replay-state.js"; +import { normalizeTextForComparison } from "./embedded-agent-helpers.js"; +import { isMessagingTool, isMessagingToolSendAction } from "./embedded-agent-messaging.js"; +import type { MessagingToolSourceReplyPayload } from "./embedded-agent-messaging.types.js"; +import { mergeEmbeddedRunReplayState } from "./embedded-agent-runner/replay-state.js"; import type { ToolCallSummary, ToolHandlerContext, -} from "./pi-embedded-subscribe.handlers.types.js"; -import { isPromiseLike } from "./pi-embedded-subscribe.promise.js"; +} from "./embedded-agent-subscribe.handlers.types.js"; +import { isPromiseLike } from "./embedded-agent-subscribe.promise.js"; import { extractToolResultMediaArtifact, extractToolErrorCode, @@ -51,16 +49,18 @@ import { isToolResultTimedOut, sanitizeToolArgs, sanitizeToolResult, -} from "./pi-embedded-subscribe.tools.js"; -import { inferToolMetaFromArgs } from "./pi-embedded-utils.js"; -import { REQUIRED_PARAM_GROUPS, type RequiredParamGroup } from "./pi-tools.params.js"; +} from "./embedded-agent-subscribe.tools.js"; +import { inferToolMetaFromArgs } from "./embedded-agent-utils.js"; +import { parseExecApprovalResultText } from "./exec-approval-result.js"; +import type { AgentEvent } from "./runtime/index.js"; +import { REQUIRED_PARAM_GROUPS, type RequiredParamGroup } from "./agent-tools.params.js"; import { buildToolMutationState, isSameToolMutationAction } from "./tool-mutation.js"; import { normalizeToolName } from "./tool-policy.js"; type ExecApprovalReplyModule = typeof import("../infra/exec-approval-reply.js"); type HookRunnerGlobalModule = typeof import("../plugins/hook-runner-global.js"); type MediaParseModule = typeof import("../media/parse.js"); -type BeforeToolCallModule = typeof import("./pi-tools.before-tool-call.js"); +type BeforeToolCallModule = typeof import("./agent-tools.before-tool-call.js"); const execApprovalReplyModuleLoader = createLazyImportLoader( () => import("../infra/exec-approval-reply.js"), @@ -72,7 +72,7 @@ const mediaParseModuleLoader = createLazyImportLoader( () => import("../media/parse.js"), ); const beforeToolCallModuleLoader = createLazyImportLoader( - () => import("./pi-tools.before-tool-call.js"), + () => import("./agent-tools.before-tool-call.js"), ); const LIVE_EXEC_OUTPUT_MAX_CHARS = 8000; const LIVE_EXEC_UPDATE_MIN_INTERVAL_MS = 250; @@ -870,7 +870,7 @@ export function handleToolExecutionStart( phase: "tool_execution_started", tool: toolName, toolCallId, - source: "pi-embedded", + source: "embedded-agent", }); // Track start time and args for after_tool_call hook. @@ -1267,7 +1267,9 @@ export async function handleToolExecutionEnd( ctx.state.successfulCronAdds += 1; } if (!isToolError && toolName === HEARTBEAT_RESPONSE_TOOL_NAME) { - const response = normalizeHeartbeatToolResponse(result?.details); + const details = + result && typeof result === "object" ? (result as { details?: unknown }).details : undefined; + const response = normalizeHeartbeatToolResponse(details); if (response) { const isFirstHeartbeatResponse = ctx.state.heartbeatToolResponse === undefined; ctx.state.heartbeatToolResponse = response; diff --git a/src/agents/pi-embedded-subscribe.handlers.ts b/src/agents/embedded-agent-subscribe.handlers.ts similarity index 85% rename from src/agents/pi-embedded-subscribe.handlers.ts rename to src/agents/embedded-agent-subscribe.handlers.ts index 9c23e5c693e..9697bcb329e 100644 --- a/src/agents/pi-embedded-subscribe.handlers.ts +++ b/src/agents/embedded-agent-subscribe.handlers.ts @@ -3,28 +3,28 @@ import { handleAgentStart, handleCompactionEnd, handleCompactionStart, -} from "./pi-embedded-subscribe.handlers.lifecycle.js"; +} from "./embedded-agent-subscribe.handlers.lifecycle.js"; import { handleMessageEnd, handleMessageStart, handleMessageUpdate, -} from "./pi-embedded-subscribe.handlers.messages.js"; +} from "./embedded-agent-subscribe.handlers.messages.js"; import { handleToolExecutionEnd, handleToolExecutionStart, handleToolExecutionUpdate, -} from "./pi-embedded-subscribe.handlers.tools.js"; +} from "./embedded-agent-subscribe.handlers.tools.js"; import type { - EmbeddedPiSubscribeContext, - EmbeddedPiSubscribeEvent, -} from "./pi-embedded-subscribe.handlers.types.js"; -import { isPromiseLike } from "./pi-embedded-subscribe.promise.js"; + EmbeddedAgentSubscribeContext, + EmbeddedAgentSubscribeEvent, +} from "./embedded-agent-subscribe.handlers.types.js"; +import { isPromiseLike } from "./embedded-agent-subscribe.promise.js"; -export function createEmbeddedPiSessionEventHandler(ctx: EmbeddedPiSubscribeContext) { +export function createEmbeddedAgentSessionEventHandler(ctx: EmbeddedAgentSubscribeContext) { let pendingEventChain: Promise | null = null; const scheduleEvent = ( - evt: EmbeddedPiSubscribeEvent, + evt: EmbeddedAgentSubscribeEvent, handler: () => void | Promise, options?: { detach?: boolean }, ): void => { @@ -72,7 +72,7 @@ export function createEmbeddedPiSessionEventHandler(ctx: EmbeddedPiSubscribeCont } }; - return (evt: EmbeddedPiSubscribeEvent) => { + return (evt: EmbeddedAgentSubscribeEvent) => { switch (evt.type) { case "message_start": scheduleEvent(evt, () => { diff --git a/src/agents/pi-embedded-subscribe.handlers.types.ts b/src/agents/embedded-agent-subscribe.handlers.types.ts similarity index 90% rename from src/agents/pi-embedded-subscribe.handlers.types.ts rename to src/agents/embedded-agent-subscribe.handlers.types.ts index d921162bc66..ac66208ea72 100644 --- a/src/agents/pi-embedded-subscribe.handlers.types.ts +++ b/src/agents/embedded-agent-subscribe.handlers.types.ts @@ -1,24 +1,24 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent"; import type { HeartbeatToolResponse } from "../auto-reply/heartbeat-tool-response.js"; import type { ReplyDirectiveParseResult } from "../auto-reply/reply/reply-directives.js"; import type { ReasoningLevel } from "../auto-reply/thinking.js"; import type { InlineCodeState } from "../markdown/code-spans.js"; import type { HookRunner } from "../plugins/hooks.js"; import type { AcceptedSessionSpawn } from "./accepted-session-spawn.js"; -import type { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js"; +import type { EmbeddedBlockChunker } from "./embedded-agent-block-chunker.js"; import type { MessagingToolSend, MessagingToolSourceReplyPayload, -} from "./pi-embedded-messaging.types.js"; -import type { BlockReplyPayload } from "./pi-embedded-payloads.js"; -import type { EmbeddedRunReplayState } from "./pi-embedded-runner/replay-state.js"; -import type { EmbeddedRunLivenessState } from "./pi-embedded-runner/types.js"; +} from "./embedded-agent-messaging.types.js"; +import type { BlockReplyPayload } from "./embedded-agent-payloads.js"; +import type { EmbeddedRunReplayState } from "./embedded-agent-runner/replay-state.js"; +import type { EmbeddedRunLivenessState } from "./embedded-agent-runner/types.js"; import type { BlockReplyChunking, - SubscribeEmbeddedPiSessionParams, -} from "./pi-embedded-subscribe.types.js"; + SubscribeEmbeddedAgentSessionParams, +} from "./embedded-agent-subscribe.types.js"; import type { AgentRunTimeoutPhase } from "./run-timeout-attribution.js"; +import type { AgentMessage } from "./runtime/index.js"; +import type { AgentSessionEvent } from "./sessions/index.js"; import type { ToolErrorSummary } from "./tool-error-summary.js"; import type { NormalizedUsage } from "./usage.js"; @@ -40,7 +40,7 @@ export type ToolCallSummary = { fileTarget?: import("./tool-mutation.js").FileTarget; }; -export type EmbeddedPiSubscribeState = { +export type EmbeddedAgentSubscribeState = { assistantTexts: string[]; toolMetas: Array<{ toolName?: string; meta?: string; asyncStarted?: boolean }>; acceptedSessionSpawns: AcceptedSessionSpawn[]; @@ -129,9 +129,9 @@ export type EmbeddedPiSubscribeState = { lastAssistant?: AgentMessage; }; -export type EmbeddedPiSubscribeContext = { - params: SubscribeEmbeddedPiSessionParams; - state: EmbeddedPiSubscribeState; +export type EmbeddedAgentSubscribeContext = { + params: SubscribeEmbeddedAgentSessionParams; + state: EmbeddedAgentSubscribeState; log: EmbeddedSubscribeLogger; blockChunking?: BlockReplyChunking; blockChunker: EmbeddedBlockChunker | null; @@ -196,10 +196,10 @@ export type EmbeddedPiSubscribeContext = { /** * Minimal context type for tool execution handlers. Allows * tests provide only the fields they exercise - * without needing the full `EmbeddedPiSubscribeContext`. + * without needing the full `EmbeddedAgentSubscribeContext`. */ type ToolHandlerParams = Pick< - SubscribeEmbeddedPiSessionParams, + SubscribeEmbeddedAgentSessionParams, | "runId" | "onBlockReplyFlush" | "onAgentEvent" @@ -214,7 +214,7 @@ type ToolHandlerParams = Pick< >; type ToolHandlerState = Pick< - EmbeddedPiSubscribeState, + EmbeddedAgentSubscribeState, | "toolMetaById" | "toolMetas" | "acceptedSessionSpawns" @@ -259,7 +259,7 @@ export type ToolHandlerContext = { trimMessagingToolSent: () => void; }; -export type EmbeddedPiSubscribeEvent = +export type EmbeddedAgentSubscribeEvent = | AgentSessionEvent | { type: string; [k: string]: unknown } | { type: "message_start"; message: AgentMessage }; diff --git a/src/agents/pi-embedded-subscribe.lifecycle-billing-error.test.ts b/src/agents/embedded-agent-subscribe.lifecycle-billing-error.test.ts similarity index 90% rename from src/agents/pi-embedded-subscribe.lifecycle-billing-error.test.ts rename to src/agents/embedded-agent-subscribe.lifecycle-billing-error.test.ts index ce263f04da4..9f96f7761c6 100644 --- a/src/agents/pi-embedded-subscribe.lifecycle-billing-error.test.ts +++ b/src/agents/embedded-agent-subscribe.lifecycle-billing-error.test.ts @@ -3,9 +3,9 @@ import { createSubscribedSessionHarness, emitAssistantLifecycleErrorAndEnd, findLifecycleErrorAgentEvent, -} from "./pi-embedded-subscribe.e2e-harness.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; -describe("subscribeEmbeddedPiSession lifecycle billing errors", () => { +describe("subscribeEmbeddedAgentSession lifecycle billing errors", () => { function createAgentEventHarness(options?: { runId?: string; sessionKey?: string }) { const onAgentEvent = vi.fn(); const { emit } = createSubscribedSessionHarness({ diff --git a/src/agents/pi-embedded-subscribe.openai-responses.test-helpers.ts b/src/agents/embedded-agent-subscribe.openai-responses.test-helpers.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.openai-responses.test-helpers.ts rename to src/agents/embedded-agent-subscribe.openai-responses.test-helpers.ts diff --git a/src/agents/pi-embedded-subscribe.promise.ts b/src/agents/embedded-agent-subscribe.promise.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.promise.ts rename to src/agents/embedded-agent-subscribe.promise.ts diff --git a/src/agents/pi-embedded-subscribe.raw-stream.ts b/src/agents/embedded-agent-subscribe.raw-stream.ts similarity index 100% rename from src/agents/pi-embedded-subscribe.raw-stream.ts rename to src/agents/embedded-agent-subscribe.raw-stream.ts diff --git a/src/agents/pi-embedded-subscribe.reply-tags.test.ts b/src/agents/embedded-agent-subscribe.reply-tags.test.ts similarity index 90% rename from src/agents/pi-embedded-subscribe.reply-tags.test.ts rename to src/agents/embedded-agent-subscribe.reply-tags.test.ts index cc2427c13de..ac0662288dc 100644 --- a/src/agents/pi-embedded-subscribe.reply-tags.test.ts +++ b/src/agents/embedded-agent-subscribe.reply-tags.test.ts @@ -1,13 +1,13 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import { createStubSessionHarness, emitAssistantTextDelta, emitAssistantTextEnd, -} from "./pi-embedded-subscribe.e2e-harness.js"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; -describe("subscribeEmbeddedPiSession reply tags", () => { +describe("subscribeEmbeddedAgentSession reply tags", () => { type ReplyPayload = { text?: string; replyToCurrent?: boolean; replyToTag?: boolean }; function replyPayloadAt(mock: ReturnType, index: number): ReplyPayload { @@ -30,7 +30,7 @@ describe("subscribeEmbeddedPiSession reply tags", () => { const { session, emit } = createStubSessionHarness(); const onBlockReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onBlockReply, @@ -87,7 +87,7 @@ describe("subscribeEmbeddedPiSession reply tags", () => { const onPartialReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onPartialReply, diff --git a/src/agents/pi-embedded-subscribe.shared-types.ts b/src/agents/embedded-agent-subscribe.shared-types.ts similarity index 65% rename from src/agents/pi-embedded-subscribe.shared-types.ts rename to src/agents/embedded-agent-subscribe.shared-types.ts index c690ba5e523..8f1c0fe699d 100644 --- a/src/agents/pi-embedded-subscribe.shared-types.ts +++ b/src/agents/embedded-agent-subscribe.shared-types.ts @@ -1,4 +1,4 @@ -import type { BlockReplyChunking } from "./pi-embedded-block-chunker.js"; +import type { BlockReplyChunking } from "./embedded-agent-block-chunker.js"; export type ToolResultFormat = "markdown" | "plain"; export type ToolProgressDetailMode = "explain" | "raw"; diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts similarity index 91% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts index 1f4e505d254..5ccb180e7b0 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.calls-onblockreplyflush-before-tool-execution-start-preserve.test.ts @@ -2,8 +2,8 @@ import { describe, expect, it, vi } from "vitest"; import { createStubSessionHarness, emitAssistantTextDelta, -} from "./pi-embedded-subscribe.e2e-harness.js"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; function firstBlockReplyText(onBlockReply: ReturnType): string | undefined { const firstCall = onBlockReply.mock.calls[0]; @@ -13,15 +13,15 @@ function firstBlockReplyText(onBlockReply: ReturnType): string | u return firstCall[0]?.text; } -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("calls onBlockReplyFlush before tool_execution_start to preserve message boundaries", () => { const { session, emit } = createStubSessionHarness(); const onBlockReplyFlush = vi.fn(); const onBlockReply = vi.fn(); - subscribeEmbeddedPiSession({ - session: session as unknown as Parameters[0]["session"], + subscribeEmbeddedAgentSession({ + session: session as unknown as Parameters[0]["session"], runId: "run-flush-test", onBlockReply, onBlockReplyFlush, @@ -64,8 +64,8 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); const onBlockReplyFlush = vi.fn(); - subscribeEmbeddedPiSession({ - session: session as unknown as Parameters[0]["session"], + subscribeEmbeddedAgentSession({ + session: session as unknown as Parameters[0]["session"], runId: "run-flush-buffer", onBlockReply, onBlockReplyFlush, @@ -100,8 +100,8 @@ describe("subscribeEmbeddedPiSession", () => { const delivered: string[] = []; const flushSnapshots: string[][] = []; - subscribeEmbeddedPiSession({ - session: session as unknown as Parameters[0]["session"], + subscribeEmbeddedAgentSession({ + session: session as unknown as Parameters[0]["session"], runId: "run-async-tool-flush", onBlockReply: async (payload) => { await Promise.resolve(); @@ -140,8 +140,8 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); const onBlockReplyFlush = vi.fn(); - subscribeEmbeddedPiSession({ - session: session as unknown as Parameters[0]["session"], + subscribeEmbeddedAgentSession({ + session: session as unknown as Parameters[0]["session"], runId: "run-message-end-flush", onBlockReply, onBlockReplyFlush, @@ -174,8 +174,8 @@ describe("subscribeEmbeddedPiSession", () => { const delivered: string[] = []; const flushSnapshots: string[][] = []; - subscribeEmbeddedPiSession({ - session: session as unknown as Parameters[0]["session"], + subscribeEmbeddedAgentSession({ + session: session as unknown as Parameters[0]["session"], runId: "run-async-message-end-flush", onBlockReply: async (payload) => { await Promise.resolve(); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-append-text-end-content-is.test.ts similarity index 98% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-append-text-end-content-is.test.ts index fb50175f933..132b95baad7 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-append-text-end-content-is.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-append-text-end-content-is.test.ts @@ -4,9 +4,9 @@ import { extractTextPayloads, emitAssistantTextDelta, emitAssistantTextEnd, -} from "./pi-embedded-subscribe.e2e-harness.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { function setupTextEndSubscription() { const onBlockReply = vi.fn(); const { emit, subscription } = createTextEndBlockReplyHarness({ onBlockReply }); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-call-onblockreplyflush-callback-is-not.test.ts similarity index 82% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-call-onblockreplyflush-callback-is-not.test.ts index ee762a46be4..30d7facac3e 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-call-onblockreplyflush-callback-is-not.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-call-onblockreplyflush-callback-is-not.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; type StubSession = { subscribe: (fn: (evt: unknown) => void) => () => void; @@ -7,7 +7,7 @@ type StubSession = { type SessionEventHandler = (evt: unknown) => void; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("does not call onBlockReplyFlush when callback is not provided", () => { let handler: SessionEventHandler | undefined; const session: StubSession = { @@ -20,8 +20,8 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); // No onBlockReplyFlush provided - subscribeEmbeddedPiSession({ - session: session as unknown as Parameters[0]["session"], + subscribeEmbeddedAgentSession({ + session: session as unknown as Parameters[0]["session"], runId: "run-no-flush", onBlockReply, blockReplyBreak: "text_end", diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-duplicate-text-end-repeats-full.test.ts similarity index 93% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-duplicate-text-end-repeats-full.test.ts index b6324397f49..cd72659581b 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-duplicate-text-end-repeats-full.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-duplicate-text-end-repeats-full.test.ts @@ -4,9 +4,9 @@ import { emitAssistantTextDelta, emitAssistantTextEnd, extractTextPayloads, -} from "./pi-embedded-subscribe.e2e-harness.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("does not duplicate when text_end repeats full content", async () => { const onBlockReply = vi.fn(); const { emit, subscription } = createTextEndBlockReplyHarness({ onBlockReply }); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-emit-duplicate-block-replies-text.test.ts similarity index 87% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-emit-duplicate-block-replies-text.test.ts index 96d68951ce9..e2e5f38bdca 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.does-not-emit-duplicate-block-replies-text.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.does-not-emit-duplicate-block-replies-text.test.ts @@ -1,14 +1,14 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import { createStubSessionHarness, createTextEndBlockReplyHarness, emitAssistantTextDelta, emitAssistantTextEnd, -} from "./pi-embedded-subscribe.e2e-harness.js"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("does not emit duplicate block replies when text_end repeats", async () => { const onBlockReply = vi.fn(); const { emit, subscription } = createTextEndBlockReplyHarness({ onBlockReply }); @@ -24,7 +24,7 @@ describe("subscribeEmbeddedPiSession", () => { it("does not duplicate assistantTexts when message_end repeats", () => { const { session, emit } = createStubSessionHarness(); - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId: "run", }); @@ -42,7 +42,7 @@ describe("subscribeEmbeddedPiSession", () => { it("does not duplicate assistantTexts when message_end repeats with trailing whitespace changes", () => { const { session, emit } = createStubSessionHarness(); - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId: "run", }); @@ -65,7 +65,7 @@ describe("subscribeEmbeddedPiSession", () => { it("does not duplicate assistantTexts when message_end repeats with reasoning blocks", () => { const { session, emit } = createStubSessionHarness(); - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId: "run", reasoningMode: "on", @@ -89,7 +89,7 @@ describe("subscribeEmbeddedPiSession", () => { // must still populate assistantTexts so providers can deliver a final reply. const { session, emit } = createStubSessionHarness(); - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId: "run", blockReplyChunking: { minChars: 50, maxChars: 200 }, // Chunking enabled diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.emits-block-replies-text-end-does-not.test.ts similarity index 97% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.emits-block-replies-text-end-does-not.test.ts index 5a9d49c2e8b..73707b2b590 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-block-replies-text-end-does-not.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.emits-block-replies-text-end-does-not.test.ts @@ -1,15 +1,15 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import { createTextEndBlockReplyHarness, emitAssistantTextDelta, emitAssistantTextEnd, -} from "./pi-embedded-subscribe.e2e-harness.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; import { createOpenAiResponsesTextBlock, createOpenAiResponsesTextEvent, type OpenAiResponsesTextEventPhase, -} from "./pi-embedded-subscribe.openai-responses.test-helpers.js"; +} from "./embedded-agent-subscribe.openai-responses.test-helpers.js"; type TextEndBlockReplyHarness = ReturnType; type OnBlockReplyMock = ReturnType; @@ -112,7 +112,7 @@ function requireBlockReplyPayload(onBlockReply: OnBlockReplyMock): BlockReplyPay return payload as BlockReplyPayload; } -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("emits block replies on text_end and does not duplicate on message_end", async () => { const onBlockReply = vi.fn(); const { emit, subscription } = createTextEndBlockReplyHarness({ onBlockReply }); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.emits-reasoning-as-separate-message-enabled.test.ts similarity index 90% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.emits-reasoning-as-separate-message-enabled.test.ts index 20afa1c4544..7b1529c6b3d 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.emits-reasoning-as-separate-message-enabled.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.emits-reasoning-as-separate-message-enabled.test.ts @@ -1,18 +1,18 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import { THINKING_TAG_CASES, createReasoningFinalAnswerMessage, createStubSessionHarness, -} from "./pi-embedded-subscribe.e2e-harness.js"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { function createReasoningBlockReplyHarness(params: { thinkingLevel?: "off" | "medium" } = {}) { const { session, emit } = createStubSessionHarness(); const onBlockReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onBlockReply, diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.filters-final-suppresses-output-without-start-tag.test.ts similarity index 93% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.filters-final-suppresses-output-without-start-tag.test.ts index f1c1ff6e7aa..9da8305ebe0 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.filters-final-suppresses-output-without-start-tag.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.filters-final-suppresses-output-without-start-tag.test.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import { createStubSessionHarness, @@ -6,8 +6,8 @@ import { emitAssistantTextEnd, emitMessageStartAndEndForAssistantText, extractAgentEventPayloads, -} from "./pi-embedded-subscribe.e2e-harness.js"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; type ReplyMock = ReturnType; type ReplyPayload = { text?: string }; @@ -24,14 +24,14 @@ function requireFirstReplyPayload(mock: ReplyMock): ReplyPayload { return payload as ReplyPayload; } -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("filters to and suppresses output without a start tag", () => { const { session, emit } = createStubSessionHarness(); const onPartialReply = vi.fn(); const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", enforceFinalTag: true, @@ -58,7 +58,7 @@ describe("subscribeEmbeddedPiSession", () => { const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", enforceFinalTag: true, @@ -76,7 +76,7 @@ describe("subscribeEmbeddedPiSession", () => { const onPartialReply = vi.fn(); const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", enforceFinalTag: true, @@ -100,7 +100,7 @@ describe("subscribeEmbeddedPiSession", () => { const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onAgentEvent, @@ -124,7 +124,7 @@ describe("subscribeEmbeddedPiSession", () => { const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onAgentEvent, @@ -147,7 +147,7 @@ describe("subscribeEmbeddedPiSession", () => { const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onAgentEvent, @@ -169,7 +169,7 @@ describe("subscribeEmbeddedPiSession", () => { const onPartialReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", enforceFinalTag: true, @@ -194,7 +194,7 @@ describe("subscribeEmbeddedPiSession", () => { const onPartialReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", enforceFinalTag: true, @@ -219,7 +219,7 @@ describe("subscribeEmbeddedPiSession", () => { const onPartialReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", enforceFinalTag: true, @@ -243,7 +243,7 @@ describe("subscribeEmbeddedPiSession", () => { const onPartialReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", enforceFinalTag: true, @@ -262,7 +262,7 @@ describe("subscribeEmbeddedPiSession", () => { const onPartialReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", enforceFinalTag: true, @@ -286,7 +286,7 @@ describe("subscribeEmbeddedPiSession", () => { const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onAgentEvent, @@ -304,7 +304,7 @@ describe("subscribeEmbeddedPiSession", () => { const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onAgentEvent, @@ -323,7 +323,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onBlockReply, @@ -344,7 +344,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onBlockReply, @@ -375,7 +375,7 @@ describe("subscribeEmbeddedPiSession", () => { const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onAgentEvent, @@ -391,7 +391,7 @@ describe("subscribeEmbeddedPiSession", () => { const onPartialReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onPartialReply, @@ -407,7 +407,7 @@ describe("subscribeEmbeddedPiSession", () => { const onBlockReply = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onBlockReply, diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.includes-canvas-action-metadata-tool-summaries.test.ts similarity index 93% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.includes-canvas-action-metadata-tool-summaries.test.ts index 8ec6c711032..ca7e3582af3 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.includes-canvas-action-metadata-tool-summaries.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.includes-canvas-action-metadata-tool-summaries.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; -import { createSubscribedSessionHarness } from "./pi-embedded-subscribe.e2e-harness.js"; +import { createSubscribedSessionHarness } from "./embedded-agent-subscribe.e2e-harness.js"; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("includes canvas action metadata in tool summaries", async () => { const onToolResult = vi.fn(); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts similarity index 84% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts index 87f824473d7..aa03718c599 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.keeps-assistanttexts-final-answer-block-replies-are.test.ts @@ -4,14 +4,14 @@ import { createStubSessionHarness, emitAssistantTextDelta, emitAssistantTextEnd, -} from "./pi-embedded-subscribe.e2e-harness.js"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("keeps assistantTexts to the final answer when block replies are disabled", () => { const { session, emit } = createStubSessionHarness(); - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId: "run", reasoningMode: "on", @@ -33,7 +33,7 @@ describe("subscribeEmbeddedPiSession", () => { const onPartialReply = vi.fn(); - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId: "run", reasoningMode: "on", diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.keeps-indented-fenced-blocks-intact.test.ts similarity index 92% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.keeps-indented-fenced-blocks-intact.test.ts index 0c62b7ed591..37eecfe0b3b 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.keeps-indented-fenced-blocks-intact.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.keeps-indented-fenced-blocks-intact.test.ts @@ -3,9 +3,9 @@ import { createParagraphChunkedBlockReplyHarness, emitAssistantTextDeltaAndEnd, extractTextPayloads, -} from "./pi-embedded-subscribe.e2e-harness.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("keeps indented fenced blocks intact", () => { const onBlockReply = vi.fn(); const { emit } = createParagraphChunkedBlockReplyHarness({ diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.reopens-fenced-blocks-splitting-inside-them.test.ts similarity index 91% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.reopens-fenced-blocks-splitting-inside-them.test.ts index 9f77f8f456a..ea493719fd3 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.reopens-fenced-blocks-splitting-inside-them.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.reopens-fenced-blocks-splitting-inside-them.test.ts @@ -3,9 +3,9 @@ import { createParagraphChunkedBlockReplyHarness, emitAssistantTextDeltaAndEnd, expectFencedChunks, -} from "./pi-embedded-subscribe.e2e-harness.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("reopens fenced blocks when splitting inside them", () => { const onBlockReply = vi.fn(); const { emit } = createParagraphChunkedBlockReplyHarness({ diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.splits-long-single-line-fenced-blocks-reopen.test.ts similarity index 85% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.splits-long-single-line-fenced-blocks-reopen.test.ts index 803463de96a..1a1d0a8dee9 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.splits-long-single-line-fenced-blocks-reopen.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.splits-long-single-line-fenced-blocks-reopen.test.ts @@ -1,16 +1,16 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import { createParagraphChunkedBlockReplyHarness, emitAssistantTextDeltaAndEnd, expectFencedChunks, -} from "./pi-embedded-subscribe.e2e-harness.js"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; import { makeZeroUsageSnapshot } from "./usage.js"; type SessionEventHandler = (evt: unknown) => void; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("splits long single-line fenced blocks with reopen/close", async () => { const onBlockReply = vi.fn(); const { emit } = createParagraphChunkedBlockReplyHarness({ @@ -38,9 +38,9 @@ describe("subscribeEmbeddedPiSession", () => { } }; }, - } as unknown as Parameters[0]["session"]; + } as unknown as Parameters[0]["session"]; - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId: "run-1", }); @@ -88,9 +88,9 @@ describe("subscribeEmbeddedPiSession", () => { listeners.push(listener); return () => {}; }, - } as unknown as Parameters[0]["session"]; + } as unknown as Parameters[0]["session"]; - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId: "run-2", }); @@ -139,9 +139,9 @@ describe("subscribeEmbeddedPiSession", () => { listeners.push(listener); return () => {}; }, - } as unknown as Parameters[0]["session"]; + } as unknown as Parameters[0]["session"]; - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run-3", }); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.streams-soft-chunks-paragraph-preference.test.ts similarity index 93% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.streams-soft-chunks-paragraph-preference.test.ts index c2183e12261..0c2ab570277 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.streams-soft-chunks-paragraph-preference.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.streams-soft-chunks-paragraph-preference.test.ts @@ -2,13 +2,13 @@ import { describe, expect, it, vi } from "vitest"; import { createParagraphChunkedBlockReplyHarness, emitAssistantTextDeltaAndEnd, -} from "./pi-embedded-subscribe.e2e-harness.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; function blockReplyTexts(onBlockReply: ReturnType): string[] { return onBlockReply.mock.calls.map(([payload]) => (payload as { text?: string }).text ?? ""); } -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("streams soft chunks with paragraph preference", () => { const onBlockReply = vi.fn(); const { emit, subscription } = createParagraphChunkedBlockReplyHarness({ diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.subscribeembeddedagentsession.test.ts similarity index 94% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.subscribeembeddedagentsession.test.ts index 5805cf6f797..8094bd276ea 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.subscribeembeddedpisession.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.subscribeembeddedagentsession.test.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import { HEARTBEAT_RESPONSE_TOOL_NAME } from "../auto-reply/heartbeat-tool-response.js"; import * as agentEvents from "../infra/agent-events.js"; @@ -11,11 +11,11 @@ import { expectSingleAgentEventText, extractAgentEventPayloads, findLifecycleErrorAgentEvent, -} from "./pi-embedded-subscribe.e2e-harness.js"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; import { makeZeroUsageSnapshot } from "./usage.js"; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { async function flushBlockReplyCallbacks(): Promise { await Promise.resolve(); await Promise.resolve(); @@ -25,7 +25,7 @@ describe("subscribeEmbeddedPiSession", () => { const { session, emit } = createStubSessionHarness(); const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: options?.runId ?? "run", onAgentEvent, @@ -37,7 +37,7 @@ describe("subscribeEmbeddedPiSession", () => { function createToolErrorHarness(runId: string) { const { session, emit } = createStubSessionHarness(); - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId, sessionKey: "test-session", @@ -47,10 +47,10 @@ describe("subscribeEmbeddedPiSession", () => { } function createSubscribedHarness( - options: Omit[0], "session">, + options: Omit[0], "session">, ) { const { session, emit } = createStubSessionHarness(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, ...options, trustedLocalMediaToolNames: options.trustedLocalMediaToolNames ?? options.builtinToolNames, @@ -277,61 +277,6 @@ describe("subscribeEmbeddedPiSession", () => { }, ); - it("suppresses assistant streaming while deterministic exec approval delivery is pending", async () => { - let resolveToolResult: (() => void) | undefined; - const onToolResult = vi.fn( - () => - new Promise((resolve) => { - resolveToolResult = resolve; - }), - ); - const onPartialReply = vi.fn(); - - const { emit } = createSubscribedHarness({ - runId: "run", - onToolResult, - onPartialReply, - }); - - emit({ - type: "tool_execution_start", - toolName: "exec", - toolCallId: "tool-1", - args: { command: "echo hi" }, - }); - emit({ - type: "tool_execution_end", - toolName: "exec", - toolCallId: "tool-1", - isError: false, - result: { - details: { - status: "approval-pending", - approvalId: "12345678-1234-1234-1234-123456789012", - approvalSlug: "12345678", - host: "gateway", - command: "echo hi", - }, - }, - }); - - emit({ - type: "message_start", - message: { role: "assistant" }, - }); - emitAssistantTextDelta(emit, "After tool"); - - await vi.waitFor(() => { - expect(onToolResult).toHaveBeenCalledTimes(1); - }); - expect(onPartialReply).not.toHaveBeenCalled(); - - expect(resolveToolResult).toBeTypeOf("function"); - resolveToolResult?.(); - await Promise.resolve(); - expect(onPartialReply).not.toHaveBeenCalled(); - }); - it("blocks local MEDIA urls from case-variant tool names in verbose output", async () => { const onToolResult = vi.fn(); const { emit } = createSubscribedHarness({ @@ -942,7 +887,7 @@ describe("subscribeEmbeddedPiSession", () => { const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run", onAgentEvent, @@ -1114,7 +1059,7 @@ describe("subscribeEmbeddedPiSession", () => { const { session, emit } = createStubSessionHarness(); const onAgentEvent = vi.fn(); - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId: "run-replay-invalid-compaction", onAgentEvent, @@ -1152,7 +1097,7 @@ describe("subscribeEmbeddedPiSession", () => { const { session, emit } = createStubSessionHarness(); const onAgentEvent = vi.fn(); - subscribeEmbeddedPiSession({ + subscribeEmbeddedAgentSession({ session, runId: "run-cron-side-effect-compaction", onAgentEvent, @@ -1181,7 +1126,7 @@ describe("subscribeEmbeddedPiSession", () => { it("preserves accepted session spawn terminal evidence across compaction retries", () => { const { session, emit } = createStubSessionHarness(); const onAgentEvent = vi.fn(); - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId: "run-spawn-side-effect-compaction", onAgentEvent, diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-commentary-phase-output.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.suppresses-commentary-phase-output.test.ts similarity index 90% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-commentary-phase-output.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.suppresses-commentary-phase-output.test.ts index ab25c4ad7b2..a2118bcf8ed 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-commentary-phase-output.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.suppresses-commentary-phase-output.test.ts @@ -1,12 +1,12 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; -import { createSubscribedSessionHarness } from "./pi-embedded-subscribe.e2e-harness.js"; +import { createSubscribedSessionHarness } from "./embedded-agent-subscribe.e2e-harness.js"; type AssistantMessageWithPhase = AssistantMessage & { phase?: "commentary" | "final_answer"; }; -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("suppresses commentary-phase assistant messages before tool use", () => { const onBlockReply = vi.fn(); const onPartialReply = vi.fn(); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.suppresses-message-end-block-replies-message-tool.test.ts similarity index 95% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.suppresses-message-end-block-replies-message-tool.test.ts index 9c5a8526f0b..9b782eb5426 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.suppresses-message-end-block-replies-message-tool.test.ts @@ -1,17 +1,17 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import { createSubscribedSessionHarness, createStubSessionHarness, emitAssistantTextDelta, emitAssistantTextEnd, -} from "./pi-embedded-subscribe.e2e-harness.js"; -import { subscribeEmbeddedPiSession } from "./pi-embedded-subscribe.js"; +} from "./embedded-agent-subscribe.e2e-harness.js"; +import { subscribeEmbeddedAgentSession } from "./embedded-agent-subscribe.js"; function createBlockReplyHarness(blockReplyBreak: "message_end" | "text_end") { const { session, emit } = createStubSessionHarness(); const onBlockReply = vi.fn(); - const subscription = subscribeEmbeddedPiSession({ + const subscription = subscribeEmbeddedAgentSession({ session, runId: "run", onBlockReply, @@ -63,7 +63,7 @@ function emitAssistantTextEndBlock(emit: (evt: unknown) => void, text: string) { emitAssistantTextEnd({ emit }); } -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("suppresses message_end block replies when the message tool already sent", async () => { const { emit, onBlockReply } = createBlockReplyHarness("message_end"); diff --git a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.waits-multiple-compaction-retries-before-resolving.test.ts similarity index 98% rename from src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts rename to src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.waits-multiple-compaction-retries-before-resolving.test.ts index da596f43f24..d6d366f91c4 100644 --- a/src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.waits-multiple-compaction-retries-before-resolving.test.ts +++ b/src/agents/embedded-agent-subscribe.subscribe-embedded-agent-session.waits-multiple-compaction-retries-before-resolving.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { onAgentEvent } from "../infra/agent-events.js"; -import { createSubscribedSessionHarness } from "./pi-embedded-subscribe.e2e-harness.js"; +import { createSubscribedSessionHarness } from "./embedded-agent-subscribe.e2e-harness.js"; function toolResultPayloadAt( onToolResult: ReturnType, @@ -15,7 +15,7 @@ function toolResultPayloadAt( return payload as { text?: string }; } -describe("subscribeEmbeddedPiSession", () => { +describe("subscribeEmbeddedAgentSession", () => { it("waits for multiple compaction retries before resolving", async () => { const { emit, subscription } = createSubscribedSessionHarness({ runId: "run-3", diff --git a/src/agents/pi-embedded-subscribe.tool-text-diagnostics.ts b/src/agents/embedded-agent-subscribe.tool-text-diagnostics.ts similarity index 93% rename from src/agents/pi-embedded-subscribe.tool-text-diagnostics.ts rename to src/agents/embedded-agent-subscribe.tool-text-diagnostics.ts index 062359ad30c..d5fba38d546 100644 --- a/src/agents/pi-embedded-subscribe.tool-text-diagnostics.ts +++ b/src/agents/embedded-agent-subscribe.tool-text-diagnostics.ts @@ -1,8 +1,8 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { extractTextFromChatContent } from "../shared/chat-content.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { detectToolCallShapedText } from "../shared/text/tool-call-shaped-text.js"; -import type { EmbeddedPiSubscribeContext } from "./pi-embedded-subscribe.handlers.types.js"; +import type { EmbeddedAgentSubscribeContext } from "./embedded-agent-subscribe.handlers.types.js"; import { normalizeToolName } from "./tool-policy.js"; function hasStructuredToolInvocation(message: AssistantMessage): boolean { @@ -55,7 +55,7 @@ function isRegisteredToolName( } export function warnIfAssistantEmittedToolText( - ctx: EmbeddedPiSubscribeContext, + ctx: EmbeddedAgentSubscribeContext, assistantMessage: AssistantMessage, ) { if (hasStructuredToolInvocation(assistantMessage)) { diff --git a/src/agents/pi-embedded-subscribe.tools.extract.test.ts b/src/agents/embedded-agent-subscribe.tools.extract.test.ts similarity index 98% rename from src/agents/pi-embedded-subscribe.tools.extract.test.ts rename to src/agents/embedded-agent-subscribe.tools.extract.test.ts index b1f00f8954e..5ab0119d2db 100644 --- a/src/agents/pi-embedded-subscribe.tools.extract.test.ts +++ b/src/agents/embedded-agent-subscribe.tools.extract.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it } from "vitest"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js"; -import { extractMessagingToolSend } from "./pi-embedded-subscribe.tools.js"; +import { extractMessagingToolSend } from "./embedded-agent-subscribe.tools.js"; function normalizeTelegramMessagingTargetForTest(raw: string): string | undefined { const trimmed = raw.trim(); diff --git a/src/agents/pi-embedded-subscribe.tools.media.test.ts b/src/agents/embedded-agent-subscribe.tools.media.test.ts similarity index 98% rename from src/agents/pi-embedded-subscribe.tools.media.test.ts rename to src/agents/embedded-agent-subscribe.tools.media.test.ts index 51372b5bf5a..d366d0a61cb 100644 --- a/src/agents/pi-embedded-subscribe.tools.media.test.ts +++ b/src/agents/embedded-agent-subscribe.tools.media.test.ts @@ -4,7 +4,7 @@ import { extractToolResultMediaPaths, filterToolResultMediaUrls, isToolResultMediaTrusted, -} from "./pi-embedded-subscribe.tools.js"; +} from "./embedded-agent-subscribe.tools.js"; describe("extractToolResultMediaPaths", () => { it("returns empty array for null/undefined", () => { @@ -147,7 +147,7 @@ describe("extractToolResultMediaPaths", () => { }); it("falls back to details.path when image content exists but no MEDIA: text", () => { - // Pi SDK read tool doesn't include MEDIA: but OpenClaw imageResult + // Embedded read tool doesn't include MEDIA: but OpenClaw imageResult // sets details.path as fallback. const result = { content: [ @@ -160,7 +160,7 @@ describe("extractToolResultMediaPaths", () => { }); it("returns empty array when image content exists but no MEDIA: and no details.path", () => { - // Pi SDK read tool: has image content but no path anywhere in the result. + // Embedded read tool: has image content but no path anywhere in the result. const result = { content: [ { type: "text", text: "Read image file [image/png]" }, diff --git a/src/agents/pi-embedded-subscribe.tools.test.ts b/src/agents/embedded-agent-subscribe.tools.test.ts similarity index 99% rename from src/agents/pi-embedded-subscribe.tools.test.ts rename to src/agents/embedded-agent-subscribe.tools.test.ts index 000ed8b82b5..a6966c78007 100644 --- a/src/agents/pi-embedded-subscribe.tools.test.ts +++ b/src/agents/embedded-agent-subscribe.tools.test.ts @@ -6,7 +6,7 @@ import { extractToolErrorMessage, sanitizeToolArgs, sanitizeToolResult, -} from "./pi-embedded-subscribe.tools.js"; +} from "./embedded-agent-subscribe.tools.js"; afterEach(() => { vi.restoreAllMocks(); diff --git a/src/agents/pi-embedded-subscribe.tools.ts b/src/agents/embedded-agent-subscribe.tools.ts similarity index 98% rename from src/agents/pi-embedded-subscribe.tools.ts rename to src/agents/embedded-agent-subscribe.tools.ts index f1b51d806a8..3ae959d482e 100644 --- a/src/agents/pi-embedded-subscribe.tools.ts +++ b/src/agents/embedded-agent-subscribe.tools.ts @@ -11,8 +11,8 @@ import { import { uniqueStrings } from "../shared/string-normalization.js"; import { truncateUtf16Safe } from "../utils.js"; import { collectTextContentBlocks } from "./content-blocks.js"; -import { isMessageToolSendActionName } from "./pi-embedded-messaging.js"; -import type { MessagingToolSend } from "./pi-embedded-messaging.types.js"; +import { isMessageToolSendActionName } from "./embedded-agent-messaging.js"; +import type { MessagingToolSend } from "./embedded-agent-messaging.types.js"; import { normalizeToolName } from "./tool-policy.js"; const TOOL_RESULT_MAX_CHARS = 8000; @@ -415,7 +415,7 @@ export function filterToolResultMediaUrls( * 2. Parse `MEDIA:` directive tokens from text content blocks. * 3. Fall back to `details.path` when image content exists (legacy imageResult). * - * Returns an empty array when no media is found (e.g. Pi SDK `read` tool + * Returns an empty array when no media is found (e.g. embedded `read` tool * returns base64 image data but no file path; those need a different delivery * path like saving to a temp file). */ diff --git a/src/agents/pi-embedded-subscribe.ts b/src/agents/embedded-agent-subscribe.ts similarity index 96% rename from src/agents/pi-embedded-subscribe.ts rename to src/agents/embedded-agent-subscribe.ts index 7f744b29450..b320451697f 100644 --- a/src/agents/pi-embedded-subscribe.ts +++ b/src/agents/embedded-agent-subscribe.ts @@ -1,4 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { setReplyPayloadMetadata } from "../auto-reply/reply-payload.js"; import { parseReplyDirectives } from "../auto-reply/reply/reply-directives.js"; import { createStreamingDirectiveAccumulator } from "../auto-reply/reply/streaming-directives.js"; @@ -11,42 +10,43 @@ import { buildCodeSpanIndex, createInlineCodeState } from "../markdown/code-span import { normalizeOptionalString } from "../shared/string-coerce.js"; import { findFinalTagMatches } from "../shared/text/final-tags.js"; import { hasOrphanReasoningCloseBoundary } from "../shared/text/reasoning-tags.js"; -import { mediaUrlsFromGeneratedAttachments } from "./generated-attachments.js"; -import { EmbeddedBlockChunker } from "./pi-embedded-block-chunker.js"; +import { EmbeddedBlockChunker } from "./embedded-agent-block-chunker.js"; import { isMessagingToolDuplicateNormalized, normalizeTextForComparison, -} from "./pi-embedded-helpers.js"; -import type { BlockReplyPayload } from "./pi-embedded-payloads.js"; -import { hasCommittedMessagingToolDeliveryEvidence } from "./pi-embedded-runner/delivery-evidence.js"; +} from "./embedded-agent-helpers.js"; +import type { BlockReplyPayload } from "./embedded-agent-payloads.js"; +import { hasCommittedMessagingToolDeliveryEvidence } from "./embedded-agent-runner/delivery-evidence.js"; import { createEmbeddedRunReplayState, mergeEmbeddedRunReplayState, -} from "./pi-embedded-runner/replay-state.js"; -import type { EmbeddedRunLivenessState } from "./pi-embedded-runner/types.js"; -import { createEmbeddedPiSessionEventHandler } from "./pi-embedded-subscribe.handlers.js"; +} from "./embedded-agent-runner/replay-state.js"; +import type { EmbeddedRunLivenessState } from "./embedded-agent-runner/types.js"; +import { createEmbeddedAgentSessionEventHandler } from "./embedded-agent-subscribe.handlers.js"; import { consumePendingAssistantReplyDirectivesIntoReply, consumePendingToolMediaIntoReply, hasAssistantVisibleReply, readPendingToolMediaReply, -} from "./pi-embedded-subscribe.handlers.messages.js"; +} from "./embedded-agent-subscribe.handlers.messages.js"; import { handleToolExecutionEnd, handleToolExecutionStart, -} from "./pi-embedded-subscribe.handlers.tools.js"; +} from "./embedded-agent-subscribe.handlers.tools.js"; import type { - EmbeddedPiSubscribeContext, - EmbeddedPiSubscribeState, -} from "./pi-embedded-subscribe.handlers.types.js"; -import { isPromiseLike } from "./pi-embedded-subscribe.promise.js"; + EmbeddedAgentSubscribeContext, + EmbeddedAgentSubscribeState, +} from "./embedded-agent-subscribe.handlers.types.js"; +import { isPromiseLike } from "./embedded-agent-subscribe.promise.js"; import { buildToolLifecycleErrorResult, filterToolResultMediaUrls, -} from "./pi-embedded-subscribe.tools.js"; -import type { SubscribeEmbeddedPiSessionParams } from "./pi-embedded-subscribe.types.js"; -import { stripDowngradedToolCallText, THINKING_TAG_SCAN_RE } from "./pi-embedded-utils.js"; +} from "./embedded-agent-subscribe.tools.js"; +import type { SubscribeEmbeddedAgentSessionParams } from "./embedded-agent-subscribe.types.js"; +import { stripDowngradedToolCallText, THINKING_TAG_SCAN_RE } from "./embedded-agent-utils.js"; +import { mediaUrlsFromGeneratedAttachments } from "./generated-attachments.js"; import type { AgentRunTimeoutPhase } from "./run-timeout-attribution.js"; +import type { AgentMessage } from "./runtime/index.js"; import { hasNonzeroUsage, normalizeUsage, type UsageLike } from "./usage.js"; const STREAM_STRIPPED_BLOCK_TAG_NAMES = [ @@ -97,7 +97,7 @@ function splitTrailingBlockTagFragment( } function collectPendingMediaFromInternalEvents( - events: SubscribeEmbeddedPiSessionParams["internalEvents"], + events: SubscribeEmbeddedAgentSessionParams["internalEvents"], ): string[] { if (!events?.length) { return []; @@ -121,15 +121,15 @@ function collectPendingMediaFromInternalEvents( return pending; } -export type { SubscribeEmbeddedPiSessionParams } from "./pi-embedded-subscribe.types.js"; +export type { SubscribeEmbeddedAgentSessionParams } from "./embedded-agent-subscribe.types.js"; -export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionParams) { +export function subscribeEmbeddedAgentSession(params: SubscribeEmbeddedAgentSessionParams) { const reasoningMode = params.reasoningMode ?? "off"; const canShowReasoning = params.thinkingLevel !== "off"; const toolResultFormat = params.toolResultFormat ?? "markdown"; const useMarkdown = toolResultFormat === "markdown"; const initialPendingToolMediaUrls = collectPendingMediaFromInternalEvents(params.internalEvents); - const state: EmbeddedPiSubscribeState = { + const state: EmbeddedAgentSubscribeState = { assistantTexts: [], toolMetas: [], acceptedSessionSpawns: [], @@ -225,7 +225,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar const shouldAllowSilentTurnText = (text: string | undefined) => Boolean(text && isSilentReplyText(text, SILENT_REPLY_TOKEN)); const emitBlockReplySafely = ( - payload: Parameters>[0], + payload: Parameters>[0], options?: { assistantMessageIndex?: number }, ): boolean => { if (!params.onBlockReply) { @@ -533,7 +533,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar const blockChunker = blockChunking ? new EmbeddedBlockChunker(blockChunking) : null; // KNOWN: Provider streams are not strictly once-only or perfectly ordered. // `text_end` can repeat full content; late `text_end` can arrive after `message_end`. - // Tests: `src/agents/pi-embedded-subscribe.test.ts` (e.g. late text_end cases). + // Tests: `src/agents/embedded-agent-subscribe.test.ts` (e.g. late text_end cases). const shouldEmitToolResult = () => typeof params.shouldEmitToolResult === "function" ? params.shouldEmitToolResult() @@ -1002,7 +1002,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar } }; - const ctx: EmbeddedPiSubscribeContext = { + const ctx: EmbeddedAgentSubscribeContext = { params, state, log, @@ -1040,7 +1040,7 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar getLastCompactionTokensAfter: () => state.lastCompactionTokensAfter, }; - const sessionUnsubscribe = params.session.subscribe(createEmbeddedPiSessionEventHandler(ctx)); + const sessionUnsubscribe = params.session.subscribe(createEmbeddedAgentSessionEventHandler(ctx)); const unsubscribe = () => { if (state.unsubscribed) { diff --git a/src/agents/pi-embedded-subscribe.types.ts b/src/agents/embedded-agent-subscribe.types.ts similarity index 89% rename from src/agents/pi-embedded-subscribe.types.ts rename to src/agents/embedded-agent-subscribe.types.ts index b1cc67f350e..a67172c10f2 100644 --- a/src/agents/pi-embedded-subscribe.types.ts +++ b/src/agents/embedded-agent-subscribe.types.ts @@ -1,4 +1,3 @@ -import type { AgentSession } from "@earendil-works/pi-coding-agent"; import type { PartialReplyPayload, SourceReplyDeliveryMode, @@ -8,21 +7,22 @@ import type { ReplyPayload } from "../auto-reply/reply-payload.js"; import type { ReasoningLevel, ThinkLevel, VerboseLevel } from "../auto-reply/thinking.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { HookRunner } from "../plugins/hooks.js"; -import type { AgentInternalEvent } from "./internal-events.js"; -import type { BlockReplyPayload } from "./pi-embedded-payloads.js"; -import type { EmbeddedRunReplayState } from "./pi-embedded-runner/replay-state.js"; +import type { BlockReplyPayload } from "./embedded-agent-payloads.js"; +import type { EmbeddedRunReplayState } from "./embedded-agent-runner/replay-state.js"; import type { BlockReplyChunking, ToolProgressDetailMode, ToolResultFormat, -} from "./pi-embedded-subscribe.shared-types.js"; +} from "./embedded-agent-subscribe.shared-types.js"; +import type { AgentInternalEvent } from "./internal-events.js"; +import type { AgentSession } from "./sessions/index.js"; export type { BlockReplyChunking, ToolProgressDetailMode, ToolResultFormat, -} from "./pi-embedded-subscribe.shared-types.js"; +} from "./embedded-agent-subscribe.shared-types.js"; -export type SubscribeEmbeddedPiSessionParams = { +export type SubscribeEmbeddedAgentSessionParams = { session: AgentSession; runId: string; initialReplayState?: EmbeddedRunReplayState; diff --git a/src/agents/pi-embedded-utils.strip-model-special-tokens.test.ts b/src/agents/embedded-agent-utils.strip-model-special-tokens.test.ts similarity index 92% rename from src/agents/pi-embedded-utils.strip-model-special-tokens.test.ts rename to src/agents/embedded-agent-utils.strip-model-special-tokens.test.ts index ef0e2b32dec..82face744b7 100644 --- a/src/agents/pi-embedded-utils.strip-model-special-tokens.test.ts +++ b/src/agents/embedded-agent-utils.strip-model-special-tokens.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { stripModelSpecialTokens } from "./pi-embedded-utils.js"; +import { stripModelSpecialTokens } from "./embedded-agent-utils.js"; /** * @see https://github.com/openclaw/openclaw/issues/40020 diff --git a/src/agents/pi-embedded-utils.test.ts b/src/agents/embedded-agent-utils.test.ts similarity index 99% rename from src/agents/pi-embedded-utils.test.ts rename to src/agents/embedded-agent-utils.test.ts index b8ec8a4463e..7cac4f533ee 100644 --- a/src/agents/pi-embedded-utils.test.ts +++ b/src/agents/embedded-agent-utils.test.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { extractAssistantText, @@ -7,7 +7,7 @@ import { formatReasoningMessage, promoteThinkingTagsToBlocks, stripDowngradedToolCallText, -} from "./pi-embedded-utils.js"; +} from "./embedded-agent-utils.js"; function makeAssistantMessage( message: Omit & diff --git a/src/agents/pi-embedded-utils.ts b/src/agents/embedded-agent-utils.ts similarity index 98% rename from src/agents/pi-embedded-utils.ts rename to src/agents/embedded-agent-utils.ts index fa77786ef36..7a4e4ad0964 100644 --- a/src/agents/pi-embedded-utils.ts +++ b/src/agents/embedded-agent-utils.ts @@ -1,5 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { extractTextFromChatContent } from "../shared/chat-content.js"; import { normalizeAssistantPhase, @@ -8,7 +7,8 @@ import { } from "../shared/chat-message-content.js"; import { sanitizeAssistantVisibleText } from "../shared/text/assistant-visible-text.js"; import { stripReasoningTagsFromText } from "../shared/text/reasoning-tags.js"; -import { sanitizeUserFacingText } from "./pi-embedded-helpers/sanitize-user-facing-text.js"; +import { sanitizeUserFacingText } from "./embedded-agent-helpers/sanitize-user-facing-text.js"; +import type { AgentMessage } from "./runtime/index.js"; import { formatToolDetail, resolveToolDisplay } from "./tool-display.js"; export { diff --git a/src/agents/embedded-agent.runtime.ts b/src/agents/embedded-agent.runtime.ts new file mode 100644 index 00000000000..de33ffe47c3 --- /dev/null +++ b/src/agents/embedded-agent.runtime.ts @@ -0,0 +1,10 @@ +export { + abortAndDrainEmbeddedAgentRun, + abortEmbeddedAgentRun, + isEmbeddedAgentRunActive, + isEmbeddedAgentRunStreaming, + resolveActiveEmbeddedRunSessionId, + runEmbeddedAgent, + resolveEmbeddedSessionLane, + waitForEmbeddedAgentRunEnd, +} from "./embedded-agent.js"; diff --git a/src/agents/pi-embedded.ts b/src/agents/embedded-agent.ts similarity index 53% rename from src/agents/pi-embedded.ts rename to src/agents/embedded-agent.ts index 3a2e388cbd8..bd877010799 100644 --- a/src/agents/pi-embedded.ts +++ b/src/agents/embedded-agent.ts @@ -3,30 +3,19 @@ export type { EmbeddedAgentMeta, EmbeddedAgentRunMeta, EmbeddedAgentRunResult, - EmbeddedPiAgentMeta, - EmbeddedPiCompactResult, - EmbeddedPiRunMeta, - EmbeddedPiRunResult, -} from "./pi-embedded-runner.js"; +} from "./embedded-agent-runner.js"; export { - abortAndDrainEmbeddedPiRun, + abortAndDrainEmbeddedAgentRun, abortEmbeddedAgentRun, - abortEmbeddedPiRun, compactEmbeddedAgentSession, - compactEmbeddedPiSession, isEmbeddedAgentRunActive, isEmbeddedAgentRunStreaming, - isEmbeddedPiRunActive, - isEmbeddedPiRunStreaming, queueEmbeddedAgentMessage, - queueEmbeddedPiMessage, - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, resolveActiveEmbeddedAgentRunSessionId, resolveActiveEmbeddedRunSessionId, resolveActiveEmbeddedRunSessionIdBySessionFile, resolveEmbeddedSessionLane, runEmbeddedAgent, - runEmbeddedPiAgent, waitForEmbeddedAgentRunEnd, - waitForEmbeddedPiRunEnd, -} from "./pi-embedded-runner.js"; +} from "./embedded-agent-runner.js"; diff --git a/src/agents/execution-contract.test.ts b/src/agents/execution-contract.test.ts index d426893a5b1..3030e32a058 100644 --- a/src/agents/execution-contract.test.ts +++ b/src/agents/execution-contract.test.ts @@ -137,7 +137,7 @@ describe("resolveEffectiveExecutionContract", () => { const config: OpenClawConfig = { agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "strict-agentic", }, }, @@ -156,7 +156,7 @@ describe("resolveEffectiveExecutionContract", () => { const config: OpenClawConfig = { agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "default", }, }, @@ -175,7 +175,7 @@ describe("resolveEffectiveExecutionContract", () => { const config: OpenClawConfig = { agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "strict-agentic", }, }, diff --git a/src/agents/execution-contract.ts b/src/agents/execution-contract.ts index ab9c9bea273..13b87b8f396 100644 --- a/src/agents/execution-contract.ts +++ b/src/agents/execution-contract.ts @@ -57,7 +57,7 @@ export function isStrictAgenticSupportedProviderModel(params: { } /** - * Returns the effective execution contract for an embedded Pi run. + * Returns the effective execution contract for an embedded OpenClaw run. * * strict-agentic is a GPT-5-family openai/openai-codex-only runtime contract, * so an unsupported provider/model pair always collapses to `"default"` diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index dc317aae99a..3ee83d7c877 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "vitest"; +import { classifyFailoverSignal } from "./embedded-agent-helpers/errors.js"; import { coerceToFailoverError, describeFailoverError, @@ -8,7 +9,6 @@ import { resolveFailoverReasonFromError, resolveFailoverStatus, } from "./failover-error.js"; -import { classifyFailoverSignal } from "./pi-embedded-helpers/errors.js"; import { SessionWriteLockTimeoutError } from "./session-write-lock-error.js"; // OpenAI 429 example shape: https://help.openai.com/en/articles/5955604-how-can-i-solve-429-too-many-requests-errors @@ -488,7 +488,7 @@ describe("failover-error", () => { ).toBeNull(); }); - it("classifies bare pi-ai stream wrapper as timeout regardless of provider (#71620)", () => { + it("classifies bare shared model runtime stream wrapper as timeout regardless of provider (#71620)", () => { expect( resolveFailoverReasonFromError({ message: "An unknown error occurred", diff --git a/src/agents/failover-error.ts b/src/agents/failover-error.ts index 3efbe96fbde..6a1e5a95a93 100644 --- a/src/agents/failover-error.ts +++ b/src/agents/failover-error.ts @@ -5,9 +5,9 @@ import { isUnclassifiedNoBodyHttpSignal, type FailoverClassification, type FailoverSignal, -} from "./pi-embedded-helpers/errors.js"; -import { isTimeoutErrorMessage } from "./pi-embedded-helpers/errors.js"; -import type { FailoverReason } from "./pi-embedded-helpers/types.js"; +} from "./embedded-agent-helpers/errors.js"; +import { isTimeoutErrorMessage } from "./embedded-agent-helpers/errors.js"; +import type { FailoverReason } from "./embedded-agent-helpers/types.js"; import { isSessionWriteLockTimeoutError } from "./session-write-lock-error.js"; const ABORT_TIMEOUT_RE = /request was aborted|request aborted/i; @@ -240,7 +240,7 @@ function hasSessionWriteLockTimeout(err: unknown, seen: Set = new Set()) } function isEmbeddedAttemptSessionTakeover(err: unknown): boolean { - // Match by name to avoid importing pi-embedded-runner here (would create a cycle). + // Match by name to avoid importing embedded-agent-runner here (would create a cycle). return Boolean( err && typeof err === "object" && readErrorName(err) === "EmbeddedAttemptSessionTakeoverError", ); diff --git a/src/agents/failover-policy.test.ts b/src/agents/failover-policy.test.ts index dbe9058c6d4..7ef0e9a2d74 100644 --- a/src/agents/failover-policy.test.ts +++ b/src/agents/failover-policy.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest"; +import type { FailoverReason } from "./embedded-agent-helpers.js"; import { shouldAllowCooldownProbeForReason, shouldPreserveTransientCooldownProbeSlot, shouldUseTransientCooldownProbeSlot, } from "./failover-policy.js"; -import type { FailoverReason } from "./pi-embedded-helpers.js"; type ReasonCase = { reason: FailoverReason | null | undefined; diff --git a/src/agents/failover-policy.ts b/src/agents/failover-policy.ts index 62b1d51108d..06c1a9868d9 100644 --- a/src/agents/failover-policy.ts +++ b/src/agents/failover-policy.ts @@ -1,4 +1,4 @@ -import type { FailoverReason } from "./pi-embedded-helpers.js"; +import type { FailoverReason } from "./embedded-agent-helpers.js"; export function shouldAllowCooldownProbeForReason( reason: FailoverReason | null | undefined, diff --git a/src/agents/google-gemini-switch.live.test.ts b/src/agents/google-gemini-switch.live.test.ts index 5169a164ca9..414ae8cef20 100644 --- a/src/agents/google-gemini-switch.live.test.ts +++ b/src/agents/google-gemini-switch.live.test.ts @@ -1,4 +1,4 @@ -import { completeSimple, getModel } from "@earendil-works/pi-ai"; +import { completeSimple, getModel } from "openclaw/plugin-sdk/llm"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { isLiveTestEnabled } from "./live-test-helpers.js"; diff --git a/src/agents/harness-runtimes.test.ts b/src/agents/harness-runtimes.test.ts index cd256a8a28d..38d75dea6f5 100644 --- a/src/agents/harness-runtimes.test.ts +++ b/src/agents/harness-runtimes.test.ts @@ -69,13 +69,13 @@ describe("collectConfiguredAgentHarnessRuntimes", () => { ); }); - it("respects explicit Pi runtime policy on selectable OpenAI agent models", () => { + it("respects explicit OpenClaw runtime policy on selectable OpenAI agent models", () => { const config = { agents: { defaults: { model: { primary: "anthropic/claude-sonnet-4-6" }, models: { - "openai/gpt-5.5": { agentRuntime: { id: "pi" } }, + "openai/gpt-5.5": { agentRuntime: { id: "openclaw" } }, }, }, }, diff --git a/src/agents/harness-runtimes.ts b/src/agents/harness-runtimes.ts index e69d1048d4f..9fe49a57148 100644 --- a/src/agents/harness-runtimes.ts +++ b/src/agents/harness-runtimes.ts @@ -1,19 +1,20 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; import { isRecord } from "../utils.js"; +import { OPENCLAW_AGENT_RUNTIME_ID, isDefaultAgentRuntimeId } from "./agent-runtime-id.js"; +import { normalizeOptionalAgentRuntimeId } from "./agent-runtime-id.js"; import { resolveAgentHarnessPolicy } from "./harness/policy.js"; -import { normalizeEmbeddedAgentRuntime } from "./pi-embedded-runner/runtime.js"; import { normalizeProviderId } from "./provider-id.js"; -function normalizeRuntimeId(value: unknown): string | undefined { - if (typeof value !== "string") { - return undefined; - } - const lower = normalizeOptionalLowercaseString(value); - if (!lower) { - return undefined; - } - return normalizeOptionalLowercaseString(normalizeEmbeddedAgentRuntime(lower)); +function normalizeConfiguredRuntimeId(value: unknown): string | undefined { + return normalizeOptionalAgentRuntimeId(value); +} + +function isSelectablePluginRuntime(runtime: string | undefined): runtime is string { + return ( + !!runtime && + !isDefaultAgentRuntimeId(runtime) && + normalizeOptionalAgentRuntimeId(runtime) !== OPENCLAW_AGENT_RUNTIME_ID + ); } function listAgentModelRefs(value: unknown): string[] { @@ -79,19 +80,19 @@ function resolveConfiguredModelHarnessRuntime(params: { if (!params.includeImplicitRuntimePreferences && policy.runtimeSource === "implicit") { return undefined; } - const runtime = normalizeRuntimeId(policy.runtime); - return runtime && runtime !== "auto" && runtime !== "pi" ? runtime : undefined; + const runtime = normalizeConfiguredRuntimeId(policy.runtime); + return isSelectablePluginRuntime(runtime) ? runtime : undefined; } function pushConfiguredModelRuntimeIds(config: OpenClawConfig, runtimes: Set): void { for (const providerConfig of Object.values(config.models?.providers ?? {})) { - const providerRuntime = normalizeRuntimeId(providerConfig?.agentRuntime?.id); - if (providerRuntime && providerRuntime !== "auto" && providerRuntime !== "pi") { + const providerRuntime = normalizeConfiguredRuntimeId(providerConfig?.agentRuntime?.id); + if (isSelectablePluginRuntime(providerRuntime)) { runtimes.add(providerRuntime); } for (const modelConfig of providerConfig?.models ?? []) { - const modelRuntime = normalizeRuntimeId(modelConfig?.agentRuntime?.id); - if (modelRuntime && modelRuntime !== "auto" && modelRuntime !== "pi") { + const modelRuntime = normalizeConfiguredRuntimeId(modelConfig?.agentRuntime?.id); + if (isSelectablePluginRuntime(modelRuntime)) { runtimes.add(modelRuntime); } } @@ -104,10 +105,10 @@ function pushConfiguredModelRuntimeIds(config: OpenClawConfig, runtimes: Set): void { - const pushRuntimeId = (value: unknown) => { - const runtime = normalizeRuntimeId(value); - if (runtime && runtime !== "auto" && runtime !== "pi") { - runtimes.add(runtime); - } - }; - - pushRuntimeId(config.agents?.defaults?.agentRuntime?.id); - const agents = Array.isArray(config.agents?.list) ? config.agents.list : []; - for (const agent of agents) { - pushRuntimeId(agent.agentRuntime?.id); - } -} - export type ConfiguredAgentHarnessRuntimeOptions = { includeEnvRuntime?: boolean; includeImplicitRuntimePreferences?: boolean; - includeLegacyAgentRuntimes?: boolean; }; export function collectConfiguredAgentHarnessRuntimes( @@ -194,18 +179,14 @@ export function collectConfiguredAgentHarnessRuntimes( const runtimes = new Set(); const includeEnvRuntime = options.includeEnvRuntime ?? true; const includeImplicitRuntimePreferences = options.includeImplicitRuntimePreferences ?? true; - const includeLegacyAgentRuntimes = options.includeLegacyAgentRuntimes ?? true; if (includeEnvRuntime) { - const envRuntime = normalizeRuntimeId(env.OPENCLAW_AGENT_RUNTIME); - if (envRuntime && envRuntime !== "auto" && envRuntime !== "pi") { + const envRuntime = normalizeConfiguredRuntimeId(env.OPENCLAW_AGENT_RUNTIME); + if (isSelectablePluginRuntime(envRuntime)) { runtimes.add(envRuntime); } } pushConfiguredModelRuntimeIds(config, runtimes); - if (includeLegacyAgentRuntimes) { - pushLegacyAgentRuntimeIds(config, runtimes); - } pushConfiguredAgentModelRuntimeIds(config, runtimes, includeImplicitRuntimePreferences); return [...runtimes].toSorted((left, right) => left.localeCompare(right)); diff --git a/src/agents/harness/builtin-openclaw.ts b/src/agents/harness/builtin-openclaw.ts new file mode 100644 index 00000000000..c76fbaf6c5a --- /dev/null +++ b/src/agents/harness/builtin-openclaw.ts @@ -0,0 +1,13 @@ +import { OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST } from "../../context-engine/host-compat.js"; +import { runEmbeddedAttempt } from "../embedded-agent-runner/run/attempt.js"; +import type { AgentHarness } from "./types.js"; + +export function createOpenClawAgentHarness(): AgentHarness { + return { + id: "openclaw", + label: "OpenClaw embedded agent", + contextEngineHostCapabilities: OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST.capabilities, + supports: () => ({ supported: true, priority: 0 }), + runAttempt: runEmbeddedAttempt, + }; +} diff --git a/src/agents/harness/builtin-pi.ts b/src/agents/harness/builtin-pi.ts deleted file mode 100644 index 25dd1c1eada..00000000000 --- a/src/agents/harness/builtin-pi.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PI_EMBEDDED_CONTEXT_ENGINE_HOST } from "../../context-engine/host-compat.js"; -import { runEmbeddedAttempt } from "../pi-embedded-runner/run/attempt.js"; -import type { AgentHarness } from "./types.js"; - -export function createPiAgentHarness(): AgentHarness { - return { - id: "pi", - label: "PI embedded agent", - contextEngineHostCapabilities: PI_EMBEDDED_CONTEXT_ENGINE_HOST.capabilities, - supports: () => ({ supported: true, priority: 0 }), - runAttempt: runEmbeddedAttempt, - }; -} diff --git a/src/agents/harness/codex-app-server-extensions.ts b/src/agents/harness/codex-app-server-extensions.ts index aff8f3a7911..9ab838d4759 100644 --- a/src/agents/harness/codex-app-server-extensions.ts +++ b/src/agents/harness/codex-app-server-extensions.ts @@ -1,4 +1,3 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { listCodexAppServerExtensionFactories } from "../../plugins/codex-app-server-extension-factory.js"; import type { @@ -7,6 +6,7 @@ import type { CodexAppServerExtensionRuntime, CodexAppServerToolResultEvent, } from "../../plugins/codex-app-server-extension-types.js"; +import type { AgentToolResult } from "../runtime/index.js"; const log = createSubsystemLogger("agents/harness"); diff --git a/src/agents/harness/context-engine-lifecycle.test.ts b/src/agents/harness/context-engine-lifecycle.test.ts index e28ae2ff4ae..7ea16ceedaa 100644 --- a/src/agents/harness/context-engine-lifecycle.test.ts +++ b/src/agents/harness/context-engine-lifecycle.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it, vi } from "vitest"; import type { ContextEngine } from "../../context-engine/types.js"; import { OPENCLAW_RUNTIME_CONTEXT_CUSTOM_TYPE } from "../internal-runtime-context.js"; diff --git a/src/agents/harness/context-engine-lifecycle.ts b/src/agents/harness/context-engine-lifecycle.ts index 53abe8bc3d4..470864913e9 100644 --- a/src/agents/harness/context-engine-lifecycle.ts +++ b/src/agents/harness/context-engine-lifecycle.ts @@ -1,12 +1,12 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { MemoryCitationsMode } from "../../config/types.memory.js"; import type { ContextEngine, ContextEngineRuntimeContext } from "../../context-engine/types.js"; -import { stripRuntimeContextCustomMessages } from "../internal-runtime-context.js"; -import { runContextEngineMaintenance } from "../pi-embedded-runner/context-engine-maintenance.js"; +import { runContextEngineMaintenance } from "../embedded-agent-runner/context-engine-maintenance.js"; import { buildAfterTurnRuntimeContext, buildAfterTurnRuntimeContextFromUsage, -} from "../pi-embedded-runner/run/attempt.prompt-helpers.js"; +} from "../embedded-agent-runner/run/attempt.prompt-helpers.js"; +import { stripRuntimeContextCustomMessages } from "../internal-runtime-context.js"; +import type { AgentMessage } from "../runtime/index.js"; import type { SessionWriteLockAcquireTimeoutConfig } from "../session-write-lock.js"; export type HarnessContextEngine = ContextEngine; diff --git a/src/agents/harness/hook-helpers.ts b/src/agents/harness/hook-helpers.ts index 46cc4573acb..cf13fa16932 100644 --- a/src/agents/harness/hook-helpers.ts +++ b/src/agents/harness/hook-helpers.ts @@ -1,7 +1,7 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js"; -import { consumeAdjustedParamsForToolCall } from "../pi-tools.before-tool-call.js"; +import { consumeAdjustedParamsForToolCall } from "../agent-tools.before-tool-call.js"; +import type { AgentMessage } from "../runtime/index.js"; const log = createSubsystemLogger("agents/harness"); diff --git a/src/agents/harness/native-hook-relay.ts b/src/agents/harness/native-hook-relay.ts index d57011fe3d6..5138a8d2cea 100644 --- a/src/agents/harness/native-hook-relay.ts +++ b/src/agents/harness/native-hook-relay.ts @@ -17,7 +17,7 @@ import { hasGlobalHooks } from "../../plugins/hook-runner-global.js"; import { PluginApprovalResolutions } from "../../plugins/types.js"; import { uniqueValues } from "../../shared/string-normalization.js"; import { asBoolean } from "../../utils/boolean.js"; -import { hasBeforeToolCallPolicy, runBeforeToolCallHook } from "../pi-tools.before-tool-call.js"; +import { hasBeforeToolCallPolicy, runBeforeToolCallHook } from "../agent-tools.before-tool-call.js"; import { stableStringify } from "../stable-stringify.js"; import { resolveToolLoopDetectionConfig } from "../tool-loop-detection-config.js"; import { normalizeToolName } from "../tool-policy.js"; @@ -1537,7 +1537,7 @@ function normalizeCodexHookMetadata(rawPayload: JsonValue): NativeHookRelayInvoc if (permissionMode) { metadata.permissionMode = permissionMode; } - const stopHookActive = asBoolean(payload.stop_hook_active); + const stopHookActive = readOptionalBoolean(payload.stop_hook_active); if (stopHookActive !== undefined) { metadata.stopHookActive = stopHookActive; } @@ -1813,7 +1813,7 @@ function normalizeAllowedEvents( if (!events?.length) { return NATIVE_HOOK_RELAY_EVENTS; } - return uniqueValues(events); + return [...new Set(events)]; } function normalizePositiveInteger(value: number | undefined, fallback: number): number { @@ -1866,6 +1866,10 @@ function readOptionalString(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } +function readOptionalBoolean(value: unknown): boolean | undefined { + return typeof value === "boolean" ? value : undefined; +} + function isJsonValue(value: unknown): value is JsonValue { const stack: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }]; let nodes = 0; diff --git a/src/agents/harness/policy.ts b/src/agents/harness/policy.ts index c931b606591..0f7edebe529 100644 --- a/src/agents/harness/policy.ts +++ b/src/agents/harness/policy.ts @@ -1,13 +1,11 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; +import { AUTO_AGENT_RUNTIME_ID, type EmbeddedAgentRuntime } from "../agent-runtime-id.js"; +import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js"; import { resolveModelRuntimePolicy } from "../model-runtime-policy.js"; import { isOpenAICodexProvider, openAIProviderUsesCodexRuntimeByDefault, } from "../openai-codex-routing.js"; -import { - normalizeEmbeddedAgentRuntime, - type EmbeddedAgentRuntime, -} from "../pi-embedded-runner/runtime.js"; export type AgentHarnessPolicy = { runtime: EmbeddedAgentRuntime; @@ -29,12 +27,12 @@ export function resolveAgentHarnessPolicy(params: { agentId: params.agentId, sessionKey: params.sessionKey, }); - const configuredRuntime = configured.policy?.id?.trim(); + const configuredRuntime = normalizeOptionalAgentRuntimeId(configured.policy?.id); const runtimeSource = configured.source ?? "implicit"; const runtime = configuredRuntime && configuredRuntime !== "default" - ? normalizeEmbeddedAgentRuntime(configuredRuntime) - : "auto"; + ? configuredRuntime + : AUTO_AGENT_RUNTIME_ID; if ( openAIProviderUsesCodexRuntimeByDefault({ provider: params.provider, config: params.config }) ) { diff --git a/src/agents/harness/prompt-compaction-hook-helpers.ts b/src/agents/harness/prompt-compaction-hook-helpers.ts index a9655ffea23..dbd95fdc767 100644 --- a/src/agents/harness/prompt-compaction-hook-helpers.ts +++ b/src/agents/harness/prompt-compaction-hook-helpers.ts @@ -1,4 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { createSubsystemLogger } from "../../logging/subsystem.js"; import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js"; import type { @@ -6,6 +5,7 @@ import type { PluginHookBeforePromptBuildResult, } from "../../plugins/types.js"; import { joinPresentTextSegments } from "../../shared/text/join-segments.js"; +import type { AgentMessage } from "../runtime/index.js"; import { buildAgentHookContext, type AgentHarnessHookContext } from "./hook-context.js"; const log = createSubsystemLogger("agents/harness"); diff --git a/src/agents/harness/registry.test.ts b/src/agents/harness/registry.test.ts index 37a33ad67b7..4b1bc28a099 100644 --- a/src/agents/harness/registry.test.ts +++ b/src/agents/harness/registry.test.ts @@ -127,7 +127,9 @@ describe("agent harness registry", () => { it("keeps model-specific harnesses behind plugin registration in auto mode", () => { process.env.OPENCLAW_AGENT_RUNTIME = "auto"; - expect(selectAgentHarness({ provider: "plugin-models", modelId: "custom-1" }).id).toBe("pi"); + expect(selectAgentHarness({ provider: "plugin-models", modelId: "custom-1" }).id).toBe( + "openclaw", + ); registerAgentHarness(makeHarness("custom", { providers: ["plugin-models"] }), { ownerPluginId: "plugin-a", @@ -138,10 +140,12 @@ describe("agent harness registry", () => { ); }); - it("falls back to PI for other models", () => { + it("falls back to OpenClaw for other models", () => { process.env.OPENCLAW_AGENT_RUNTIME = "auto"; - expect(selectAgentHarness({ provider: "anthropic", modelId: "sonnet-4.6" }).id).toBe("pi"); + expect(selectAgentHarness({ provider: "anthropic", modelId: "sonnet-4.6" }).id).toBe( + "openclaw", + ); }); it("lets a plugin harness win in auto mode by priority", () => { @@ -153,7 +157,7 @@ describe("agent harness registry", () => { expect(selectAgentHarness({ provider: "codex", modelId: "gpt-5.4" }).id).toBe("plugin-harness"); }); - it("honors explicit provider PI runtime policy", () => { + it("honors explicit provider OpenClaw runtime policy", () => { registerAgentHarness(makeHarness("plugin-harness", { priority: 200 }), { ownerPluginId: "plugin-a", }); @@ -162,9 +166,9 @@ describe("agent harness registry", () => { selectAgentHarness({ provider: "codex", modelId: "gpt-5.4", - config: providerRuntimeConfig("codex", "pi"), + config: providerRuntimeConfig("codex", "openclaw"), }).id, - ).toBe("pi"); + ).toBe("openclaw"); }); it("honors explicit provider plugin runtime policy when the plugin harness is registered", () => { diff --git a/src/agents/harness/runtime-plugin.test.ts b/src/agents/harness/runtime-plugin.test.ts index 3c14e22da64..65e9ed83d8e 100644 --- a/src/agents/harness/runtime-plugin.test.ts +++ b/src/agents/harness/runtime-plugin.test.ts @@ -260,7 +260,7 @@ describe("ensureSelectedAgentHarnessPlugin", () => { ); }); - it("keeps custom OpenAI-compatible providers on Pi when no runtime override is set", async () => { + it("keeps custom OpenAI-compatible providers on embedded OpenClaw when no runtime override is set", async () => { await ensureSelectedAgentHarnessPlugin({ provider: "openai", modelId: "gpt-5.5", diff --git a/src/agents/harness/runtime-plugin.ts b/src/agents/harness/runtime-plugin.ts index 36b5286707e..8b13475c0cd 100644 --- a/src/agents/harness/runtime-plugin.ts +++ b/src/agents/harness/runtime-plugin.ts @@ -5,9 +5,24 @@ import { resolveBundledProviderCompatPluginIds, resolveOwningPluginIdsForProvider, } from "../../plugins/providers.js"; -import { normalizeUniqueStringEntries } from "../../shared/string-normalization.js"; +import { isDefaultAgentRuntimeId } from "../agent-runtime-id.js"; +import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js"; import { resolveAgentHarnessPolicy } from "./policy.js"; +function dedupePluginIds(values: readonly string[]): string[] { + const seen = new Set(); + const result: string[] = []; + for (const value of values) { + const pluginId = value.trim(); + if (!pluginId || seen.has(pluginId)) { + continue; + } + seen.add(pluginId); + result.push(pluginId); + } + return result; +} + function restrictiveAllowlistOmitsPlugin(config: OpenClawConfig | undefined, pluginId: string) { if (config?.plugins?.bundledDiscovery === "compat") { return false; @@ -24,7 +39,7 @@ function resolveCodexHarnessPluginIds(params: { if (restrictiveAllowlistOmitsPlugin(params.config, "codex")) { return ["codex"]; } - const providerOwnerPluginIds = normalizeUniqueStringEntries( + const providerOwnerPluginIds = dedupePluginIds( resolveOwningPluginIdsForProvider({ provider: params.provider, config: params.config, @@ -34,7 +49,7 @@ function resolveCodexHarnessPluginIds(params: { if (providerOwnerPluginIds.length === 0) { return ["codex"]; } - const safeProviderOwnerPluginIds = normalizeUniqueStringEntries([ + const safeProviderOwnerPluginIds = dedupePluginIds([ ...resolveBundledProviderCompatPluginIds({ config: params.config, workspaceDir: params.workspaceDir, @@ -46,7 +61,7 @@ function resolveCodexHarnessPluginIds(params: { workspaceDir: params.workspaceDir, }), ]); - return normalizeUniqueStringEntries([ + return dedupePluginIds([ "codex", ...providerOwnerPluginIds.filter( (pluginId) => pluginId !== "codex" && safeProviderOwnerPluginIds.includes(pluginId), @@ -65,10 +80,7 @@ function withRuntimePluginIdsAllowed(params: { if (restrictiveAllowlistOmitsPlugin(params.config, params.requiredPluginId)) { return params.config; } - const allow = normalizeUniqueStringEntries([ - ...(params.config?.plugins?.allow ?? []), - ...params.pluginIds, - ]); + const allow = dedupePluginIds([...(params.config?.plugins?.allow ?? []), ...params.pluginIds]); return { ...params.config, plugins: { @@ -87,7 +99,7 @@ export async function ensureSelectedAgentHarnessPlugin(params: { agentHarnessRuntimeOverride?: string; workspaceDir: string; }): Promise { - const runtimeOverride = params.agentHarnessRuntimeOverride?.trim(); + const runtimeOverride = normalizeOptionalAgentRuntimeId(params.agentHarnessRuntimeOverride); const policy = resolveAgentHarnessPolicy({ provider: params.provider, modelId: params.modelId, @@ -96,9 +108,7 @@ export async function ensureSelectedAgentHarnessPlugin(params: { sessionKey: params.sessionKey, }); const runtime = - runtimeOverride && runtimeOverride !== "auto" && runtimeOverride !== "default" - ? runtimeOverride - : policy.runtime; + runtimeOverride && !isDefaultAgentRuntimeId(runtimeOverride) ? runtimeOverride : policy.runtime; if (runtime !== "codex") { return; } diff --git a/src/agents/harness/selection.test.ts b/src/agents/harness/selection.test.ts index bdea61f592c..9e46f5e953f 100644 --- a/src/agents/harness/selection.test.ts +++ b/src/agents/harness/selection.test.ts @@ -1,11 +1,12 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; +import { OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST } from "../../context-engine/host-compat.js"; import type { ContextEngine } from "../../context-engine/types.js"; import type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult, -} from "../pi-embedded-runner/run/types.js"; +} from "../embedded-agent-runner/run/types.js"; import { clearAgentHarnesses, registerAgentHarness } from "./registry.js"; import { maybeCompactAgentHarnessSession, @@ -16,22 +17,17 @@ import { } from "./selection.js"; import type { AgentHarness } from "./types.js"; -const piRunAttempt = vi.fn(async () => createAttemptResult("pi")); +const agentRunAttempt = vi.fn(async () => + createAttemptResult("openclaw"), +); -vi.mock("./builtin-pi.js", () => ({ - createPiAgentHarness: (): AgentHarness => ({ - id: "pi", - label: "PI embedded agent", - contextEngineHostCapabilities: [ - "bootstrap", - "assemble-before-prompt", - "after-turn", - "maintain", - "compact", - "runtime-llm-complete", - ], +vi.mock("./builtin-openclaw.js", () => ({ + createOpenClawAgentHarness: (): AgentHarness => ({ + id: "openclaw", + label: "OpenClaw embedded agent", + contextEngineHostCapabilities: OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST.capabilities, supports: () => ({ supported: true, priority: 0 }), - runAttempt: piRunAttempt, + runAttempt: agentRunAttempt, }), })); @@ -43,7 +39,7 @@ beforeEach(() => { afterEach(() => { clearAgentHarnesses(); - piRunAttempt.mockClear(); + agentRunAttempt.mockClear(); if (originalRuntime == null) { delete process.env.OPENCLAW_AGENT_RUNTIME; } else { @@ -61,7 +57,7 @@ function createAttemptParams(config?: OpenClawConfig): EmbeddedRunAttemptParams timeoutMs: 5_000, provider: "codex", modelId: "gpt-5.4", - model: { id: "gpt-5.4", provider: "codex" } as Model, + model: { id: "gpt-5.4", provider: "codex" } as Model, authStorage: {} as never, authProfileStore: { version: 1, profiles: {} }, modelRegistry: {} as never, @@ -225,33 +221,33 @@ describe("runAgentHarnessAttempt", () => { await expect( runAgentHarnessAttempt(createAttemptParams(providerRuntimeConfig("codex", "codex"))), ).rejects.toThrow('Requested agent harness "codex" is not registered.'); - expect(piRunAttempt).not.toHaveBeenCalled(); + expect(agentRunAttempt).not.toHaveBeenCalled(); }); - it("falls back to the PI harness in auto mode when no plugin harness matches", async () => { + it("falls back to the OpenClaw harness in auto mode when no plugin harness matches", async () => { const result = await runAgentHarnessAttempt(createAttemptParams()); - expect(result.sessionIdUsed).toBe("pi"); - expect(piRunAttempt).toHaveBeenCalledTimes(1); + expect(result.sessionIdUsed).toBe("openclaw"); + expect(agentRunAttempt).toHaveBeenCalledTimes(1); }); - it("allows the selected PI harness to satisfy context-engine pre-prompt assembly", async () => { + it("allows the selected OpenClaw harness to satisfy context-engine pre-prompt assembly", async () => { const result = await runAgentHarnessAttempt({ - ...createAttemptParams(providerRuntimeConfig("codex", "pi")), + ...createAttemptParams(providerRuntimeConfig("codex", "openclaw")), contextEngine: createContextEngineRequiringAssembly(), }); - expect(result.sessionIdUsed).toBe("pi"); - expect(piRunAttempt).toHaveBeenCalledTimes(1); + expect(result.sessionIdUsed).toBe("openclaw"); + expect(agentRunAttempt).toHaveBeenCalledTimes(1); }); - it("surfaces an auto-selected plugin harness failure instead of replaying through PI", async () => { + it("surfaces an auto-selected plugin harness failure instead of replaying through OpenClaw", async () => { registerFailingCodexHarness(); await expect(runAgentHarnessAttempt(createAttemptParams())).rejects.toThrow( "codex startup failed", ); - expect(piRunAttempt).not.toHaveBeenCalled(); + expect(agentRunAttempt).not.toHaveBeenCalled(); }); it("auto-selects a supporting plugin harness by default", async () => { @@ -260,16 +256,16 @@ describe("runAgentHarnessAttempt", () => { await expect(runAgentHarnessAttempt(createAttemptParams())).rejects.toThrow( "codex startup failed", ); - expect(piRunAttempt).not.toHaveBeenCalled(); + expect(agentRunAttempt).not.toHaveBeenCalled(); }); - it("surfaces a forced plugin harness failure instead of replaying through PI", async () => { + it("surfaces a forced plugin harness failure instead of replaying through OpenClaw", async () => { registerFailingCodexHarness(); await expect( runAgentHarnessAttempt(createAttemptParams(providerRuntimeConfig("codex", "codex"))), ).rejects.toThrow("codex startup failed"); - expect(piRunAttempt).not.toHaveBeenCalled(); + expect(agentRunAttempt).not.toHaveBeenCalled(); }); it("rejects the candidate when the forced plugin harness does not support its provider", async () => { @@ -318,16 +314,16 @@ describe("runAgentHarnessAttempt", () => { modelId: "gpt-5.4", }); expect(result.sessionIdUsed).toBe("codex"); - expect(piRunAttempt).not.toHaveBeenCalled(); + expect(agentRunAttempt).not.toHaveBeenCalled(); }); - it("falls back to PI when the implicit OpenAI Codex harness is unavailable", async () => { + it("falls back to OpenClaw when the implicit OpenAI Codex harness is unavailable", async () => { expect(resolveAgentHarnessPolicy({ provider: "openai", modelId: "gpt-5.4" })).toEqual({ runtime: "codex", runtimeSource: "implicit", }); expect(resolveAvailableAgentHarnessPolicy({ provider: "openai", modelId: "gpt-5.4" })).toEqual({ - runtime: "pi", + runtime: "openclaw", runtimeSource: "implicit", }); @@ -337,30 +333,30 @@ describe("runAgentHarnessAttempt", () => { modelId: "gpt-5.4", }); - expect(result.sessionIdUsed).toBe("pi"); - expect(piRunAttempt).toHaveBeenCalledTimes(1); + expect(result.sessionIdUsed).toBe("openclaw"); + expect(agentRunAttempt).toHaveBeenCalledTimes(1); }); - it("honors explicit PI runtime for OpenAI agent model runs", async () => { + it("honors explicit OpenClaw runtime for OpenAI agent model runs", async () => { const result = await runAgentHarnessAttempt({ - ...createAttemptParams(providerRuntimeConfig("openai", "pi")), + ...createAttemptParams(providerRuntimeConfig("openai", "openclaw")), provider: "openai", modelId: "gpt-5.4", }); - expect(result.sessionIdUsed).toBe("pi"); - expect(piRunAttempt).toHaveBeenCalledTimes(1); + expect(result.sessionIdUsed).toBe("openclaw"); + expect(agentRunAttempt).toHaveBeenCalledTimes(1); }); - it("honors provider wildcard PI runtime policy for OpenAI agent model runs", async () => { + it("honors provider wildcard OpenClaw runtime policy for OpenAI agent model runs", async () => { registerSuccessfulCodexHarness(); const result = await runAgentHarnessAttempt({ - ...createAttemptParams(agentModelRuntimeConfig("openai/*", "pi")), + ...createAttemptParams(agentModelRuntimeConfig("openai/*", "openclaw")), provider: "openai", modelId: "gpt-5.4", }); - expect(result.sessionIdUsed).toBe("pi"); - expect(piRunAttempt).toHaveBeenCalledTimes(1); + expect(result.sessionIdUsed).toBe("openclaw"); + expect(agentRunAttempt).toHaveBeenCalledTimes(1); }); it("annotates non-ok harness result classifications for outer model fallback", async () => { @@ -443,7 +439,7 @@ describe("runAgentHarnessAttempt", () => { expect(attempt?.extraSystemPrompt).toContain("this chat is not allowed by policy"); }); - it("leaves PI harness params unchanged for channel group sender deny-all policy", async () => { + it("leaves OpenClaw harness params unchanged for channel group sender deny-all policy", async () => { await runAgentHarnessAttempt({ ...createAttemptParams(groupSenderDenyAllConfig()), sessionKey: "agent:main:telegram:group:test-deny-room", @@ -452,25 +448,25 @@ describe("runAgentHarnessAttempt", () => { senderId: "test-denied-sender", }); - expect(piRunAttempt).toHaveBeenCalledTimes(1); - expect(piRunAttempt.mock.calls[0]?.[0].toolsAllow).toBeUndefined(); + expect(agentRunAttempt).toHaveBeenCalledTimes(1); + expect(agentRunAttempt.mock.calls[0]?.[0].toolsAllow).toBeUndefined(); }); it("fails for config-forced plugin harnesses when fallback is omitted", async () => { await expect( runAgentHarnessAttempt(createAttemptParams(providerRuntimeConfig("codex", "codex"))), ).rejects.toThrow('Requested agent harness "codex" is not registered'); - expect(piRunAttempt).not.toHaveBeenCalled(); + expect(agentRunAttempt).not.toHaveBeenCalled(); }); - it("does not let a strict agent model plugin runtime fall back to PI", async () => { + it("does not let a strict agent model plugin runtime fall back to OpenClaw", async () => { await expect( runAgentHarnessAttempt({ ...createAttemptParams(agentModelRuntimeConfig("codex/gpt-5.4", "codex", "strict")), sessionKey: "agent:strict:session-1", }), ).rejects.toThrow('Requested agent harness "codex" is not registered'); - expect(piRunAttempt).not.toHaveBeenCalled(); + expect(agentRunAttempt).not.toHaveBeenCalled(); }); }); @@ -547,7 +543,7 @@ describe("selectAgentHarness", () => { expect(unsupportedSupports).toHaveBeenCalledTimes(1); }); - it("ignores session-level PI pins when selecting a harness", () => { + it("ignores session-level OpenClaw pins when selecting a harness", () => { const supports = vi.fn(() => ({ supported: true as const, priority: 100 })); registerAgentHarness({ id: "codex", @@ -559,31 +555,31 @@ describe("selectAgentHarness", () => { const harness = selectAgentHarness({ provider: "codex", modelId: "gpt-5.4", - agentHarnessId: "pi", + agentHarnessId: "openclaw", }); expect(harness.id).toBe("codex"); expect(supports).toHaveBeenCalledTimes(1); }); - it("honors explicit PI runtime overrides when selecting a harness", async () => { + it("honors explicit OpenClaw runtime overrides when selecting a harness", async () => { registerSuccessfulCodexHarness(); const harness = selectAgentHarness({ provider: "openai", modelId: "gpt-5.4", - agentHarnessRuntimeOverride: "pi", + agentHarnessRuntimeOverride: "openclaw", }); - expect(harness.id).toBe("pi"); + expect(harness.id).toBe("openclaw"); const result = await runAgentHarnessAttempt({ ...createAttemptParams(), provider: "openai", modelId: "gpt-5.4", - agentHarnessRuntimeOverride: "pi", + agentHarnessRuntimeOverride: "openclaw", }); - expect(result.sessionIdUsed).toBe("pi"); + expect(result.sessionIdUsed).toBe("openclaw"); }); it("allows per-agent model runtime policy overrides", () => { @@ -598,12 +594,12 @@ describe("selectAgentHarness", () => { }), ).toThrow('Requested agent harness "codex" is not registered'); expect(selectAgentHarness({ provider: "anthropic", modelId: "sonnet-4.6", config }).id).toBe( - "pi", + "openclaw", ); }); - it("selects PI when the implicit OpenAI Codex harness is unavailable", () => { - expect(selectAgentHarness({ provider: "openai", modelId: "gpt-5.4" }).id).toBe("pi"); + it("selects OpenClaw when the implicit OpenAI Codex harness is unavailable", () => { + expect(selectAgentHarness({ provider: "openai", modelId: "gpt-5.4" }).id).toBe("openclaw"); }); it("ignores legacy agentRuntime as a runtime policy source", () => { @@ -621,7 +617,7 @@ describe("selectAgentHarness", () => { modelId: "sonnet-4.6", config, }).id, - ).toBe("pi"); + ).toBe("openclaw"); }); it("ignores legacy agent CLI runtime aliases for OpenAI agent model runs", async () => { @@ -642,24 +638,24 @@ describe("selectAgentHarness", () => { modelId: "gpt-5.4", }); expect(result.sessionIdUsed).toBe("codex"); - expect(piRunAttempt).not.toHaveBeenCalled(); + expect(agentRunAttempt).not.toHaveBeenCalled(); }); - it("ignores existing session PI pins when provider policy forces a plugin harness", () => { + it("ignores existing session OpenClaw pins when provider policy forces a plugin harness", () => { registerFailingCodexHarness(); expect( selectAgentHarness({ provider: "codex", modelId: "gpt-5.4", - agentHarnessId: "pi", + agentHarnessId: "openclaw", config: providerRuntimeConfig("codex", "codex"), }).id, ).toBe("codex"); }); - it("ignores env-forced PI for OpenAI default runtime selection", () => { - process.env.OPENCLAW_AGENT_RUNTIME = "pi"; + it("ignores env-forced OpenClaw for OpenAI default runtime selection", () => { + process.env.OPENCLAW_AGENT_RUNTIME = "openclaw"; registerFailingCodexHarness(); expect( @@ -715,7 +711,7 @@ describe("selectAgentHarness", () => { ).resolves.toBeUndefined(); }); - it("does not compact a selected plugin harness through PI when the plugin has no compactor", async () => { + it("does not compact a selected plugin harness through OpenClaw when the plugin has no compactor", async () => { registerFailingCodexHarness(); await expect( @@ -740,7 +736,7 @@ describe("selectAgentHarness", () => { { provider: "anthropic", modelId: "sonnet-4.6", alias: "claude-cli" }, { provider: "google", modelId: "gemini-3-pro-preview", alias: "google-gemini-cli" }, ])( - "returns PI for explicit CLI runtime alias $alias on $provider instead of throwing MissingAgentHarnessError", + "returns OpenClaw for explicit CLI runtime alias $alias on $provider instead of throwing MissingAgentHarnessError", ({ provider, modelId, alias }) => { expect( selectAgentHarness({ @@ -748,7 +744,7 @@ describe("selectAgentHarness", () => { modelId, agentHarnessRuntimeOverride: alias, }).id, - ).toBe("pi"); + ).toBe("openclaw"); }, ); diff --git a/src/agents/harness/selection.ts b/src/agents/harness/selection.ts index d0e6066d0e7..e481ac863f1 100644 --- a/src/agents/harness/selection.ts +++ b/src/agents/harness/selection.ts @@ -1,19 +1,20 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; -import { isCliRuntimeAliasForProvider, isCliRuntimeProvider } from "../model-runtime-aliases.js"; -import type { CompactEmbeddedPiSessionParams } from "../pi-embedded-runner/compact.types.js"; -import type { - EmbeddedRunAttemptParams, - EmbeddedRunAttemptResult, -} from "../pi-embedded-runner/run/types.js"; -import type { EmbeddedPiCompactResult } from "../pi-embedded-runner/types.js"; +import { isDefaultAgentRuntimeId, normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js"; import { resolveEffectiveToolPolicy, resolveGroupToolPolicy, resolveInheritedToolPolicyForSession, resolveSubagentToolPolicyForSession, -} from "../pi-tools.policy.js"; +} from "../agent-tools.policy.js"; +import type { CompactEmbeddedAgentSessionParams } from "../embedded-agent-runner/compact.types.js"; +import type { + EmbeddedRunAttemptParams, + EmbeddedRunAttemptResult, +} from "../embedded-agent-runner/run/types.js"; +import type { EmbeddedAgentCompactResult } from "../embedded-agent-runner/types.js"; +import { isCliRuntimeAliasForProvider, isCliRuntimeProvider } from "../model-runtime-aliases.js"; import { resolveSandboxRuntimeStatus } from "../sandbox/runtime-status.js"; import { resolveSenderToolPolicy } from "../sender-tool-policy.js"; import { @@ -21,7 +22,7 @@ import { resolveSubagentCapabilityStore, } from "../subagent-capabilities.js"; import { expandToolGroups, normalizeToolName } from "../tool-policy.js"; -import { createPiAgentHarness } from "./builtin-pi.js"; +import { createOpenClawAgentHarness } from "./builtin-openclaw.js"; import { MissingAgentHarnessError } from "./errors.js"; import { resolveAgentHarnessPolicy as resolveConfiguredAgentHarnessPolicy, @@ -56,19 +57,16 @@ type AgentHarnessSelectionDecision = { policy: AgentHarnessPolicy; selectedHarnessId: string; selectedReason: - | "forced_pi" + | "forced_openclaw" | "forced_plugin" - // Implicit Codex preference found no registered Codex harness, so PI handled the run. - | "implicit_plugin_unavailable_pi" - // Explicit provider-owned CLI runtime aliases have no agent harness plugin - // counterpart. PI is returned as the transcript-composition placeholder; the - // actual run is routed through CLI dispatch by callers that consult model - // runtime policy (see `assertModelFallbackCandidateHarnessAvailable`). - | "cli_runtime_passthrough_pi" + // Implicit Codex preference found no registered Codex harness, so OpenClaw handled the run. + | "implicit_plugin_unavailable_openclaw" + // Provider-owned CLI runtime aliases have no agent harness plugin counterpart. + | "cli_runtime_passthrough_openclaw" // Auto mode chose a registered plugin harness that supports the provider/model. | "auto_plugin" - // Auto mode found no supporting plugin harness, so PI handled the run. - | "auto_pi"; + // Auto mode found no supporting plugin harness, so OpenClaw handled the run. + | "auto_openclaw"; candidates: AgentHarnessSelectionCandidate[]; }; @@ -95,7 +93,7 @@ function applyAgentHarnessAvailabilityPolicy(policy: AgentHarnessPolicy): AgentH ) { return { ...policy, - runtime: "pi", + runtime: "openclaw", }; } return policy; @@ -134,25 +132,25 @@ function selectAgentHarnessDecision(params: { agentHarnessRuntimeOverride?: string; }): AgentHarnessSelectionDecision { const resolvedPolicy = resolveConfiguredAgentHarnessPolicy(params); - const runtimeOverride = params.agentHarnessRuntimeOverride?.trim(); + const runtimeOverride = normalizeOptionalAgentRuntimeId(params.agentHarnessRuntimeOverride); const policy = - runtimeOverride && runtimeOverride !== "auto" && runtimeOverride !== "default" + runtimeOverride && !isDefaultAgentRuntimeId(runtimeOverride) ? ({ ...resolvedPolicy, runtime: runtimeOverride, runtimeSource: "model", } as AgentHarnessPolicy) : resolvedPolicy; - // PI is intentionally not part of the plugin candidate list. Explicit plugin - // runtimes fail closed; only `auto` may route an unmatched turn to PI. + // OpenClaw's built-in harness is intentionally not part of the plugin candidate list. Explicit plugin + // runtimes fail closed; only `auto` may route an unmatched turn to OpenClaw. const pluginHarnesses = listPluginAgentHarnesses(); - const piHarness = createPiAgentHarness(); + const openClawHarness = createOpenClawAgentHarness(); const runtime = policy.runtime; - if (runtime === "pi") { + if (runtime === "openclaw") { return buildSelectionDecision({ - harness: piHarness, + harness: openClawHarness, policy, - selectedReason: "forced_pi", + selectedReason: "forced_openclaw", candidates: listHarnessCandidates(pluginHarnesses), }); } @@ -191,23 +189,23 @@ function selectAgentHarnessDecision(params: { } if (runtime === "codex" && policy.runtimeSource === "implicit") { return buildSelectionDecision({ - harness: piHarness, + harness: openClawHarness, policy: { ...policy, - runtime: "pi", + runtime: "openclaw", }, - selectedReason: "implicit_plugin_unavailable_pi", + selectedReason: "implicit_plugin_unavailable_openclaw", candidates: listHarnessCandidates(pluginHarnesses), }); } if (isCliRuntimeAliasForProvider({ runtime, provider: params.provider })) { return buildSelectionDecision({ - harness: piHarness, + harness: openClawHarness, policy: { ...policy, - runtime: "pi", + runtime: "openclaw", }, - selectedReason: "cli_runtime_passthrough_pi", + selectedReason: "cli_runtime_passthrough_openclaw", candidates: listHarnessCandidates(pluginHarnesses), }); } @@ -243,9 +241,9 @@ function selectAgentHarnessDecision(params: { }); } return buildSelectionDecision({ - harness: piHarness, + harness: openClawHarness, policy, - selectedReason: "auto_pi", + selectedReason: "auto_openclaw", candidates: candidates.map(toSelectionCandidate), }); } @@ -263,7 +261,8 @@ export async function runAgentHarnessAttempt( agentHarnessRuntimeOverride: params.agentHarnessRuntimeOverride, }); const harness = selection.harness; - const attemptParams = harness.id === "pi" ? params : applyPluginHarnessDenyAllToolPolicy(params); + const attemptParams = + harness.id === "openclaw" ? params : applyPluginHarnessDenyAllToolPolicy(params); logAgentHarnessSelection(selection, { provider: params.provider, modelId: params.modelId, @@ -271,14 +270,14 @@ export async function runAgentHarnessAttempt( agentId: params.agentId, }); const v2Harness = adaptAgentHarnessToV2(harness); - if (harness.id === "pi") { + if (harness.id === "openclaw") { return await runAgentHarnessV2LifecycleAttempt(v2Harness, attemptParams); } try { return await runAgentHarnessV2LifecycleAttempt(v2Harness, attemptParams); } catch (error) { - log.warn(`${harness.label} failed; not falling back to embedded PI backend`, { + log.warn(`${harness.label} failed; not falling back to embedded OpenClaw backend`, { harnessId: harness.id, provider: params.provider, modelId: params.modelId, @@ -480,8 +479,8 @@ function logAgentHarnessSelection( } export async function maybeCompactAgentHarnessSession( - params: CompactEmbeddedPiSessionParams, -): Promise { + params: CompactEmbeddedAgentSessionParams, +): Promise { if (params.provider && isCliRuntimeProvider(params.provider)) { return undefined; } @@ -501,7 +500,7 @@ export async function maybeCompactAgentHarnessSession( sessionKey: params.sessionKey, }); if (!harness.compact) { - if (harness.id !== "pi") { + if (harness.id !== "openclaw") { return { ok: false, compacted: false, diff --git a/src/agents/harness/tool-result-middleware.test.ts b/src/agents/harness/tool-result-middleware.test.ts index 22fc424543d..6f12e794c3e 100644 --- a/src/agents/harness/tool-result-middleware.test.ts +++ b/src/agents/harness/tool-result-middleware.test.ts @@ -3,7 +3,7 @@ import { createAgentToolResultMiddlewareRunner } from "./tool-result-middleware. describe("createAgentToolResultMiddlewareRunner", () => { it("fails closed when middleware throws", async () => { - const runner = createAgentToolResultMiddlewareRunner({ runtime: "pi" }, [ + const runner = createAgentToolResultMiddlewareRunner({ runtime: "openclaw" }, [ () => { throw new Error("raw secret should not be logged or returned"); }, @@ -47,7 +47,7 @@ describe("createAgentToolResultMiddlewareRunner", () => { }); it("fails closed when middleware mutates the current result into an invalid shape", async () => { - const runner = createAgentToolResultMiddlewareRunner({ runtime: "pi" }, [ + const runner = createAgentToolResultMiddlewareRunner({ runtime: "openclaw" }, [ (event) => { event.result.content = "not an array" as never; return undefined; @@ -122,7 +122,7 @@ describe("createAgentToolResultMiddlewareRunner", () => { content: [{ type: "text" as const, text: "delivered" }], details: cyclicDetails, }; - const runner = createAgentToolResultMiddlewareRunner({ runtime: "pi" }, []); + const runner = createAgentToolResultMiddlewareRunner({ runtime: "openclaw" }, []); const result = await runner.applyToolResultMiddleware({ toolCallId: "call-1", @@ -147,7 +147,9 @@ describe("createAgentToolResultMiddlewareRunner", () => { client, }; client.message = payload; - const runner = createAgentToolResultMiddlewareRunner({ runtime: "pi" }, [() => undefined]); + const runner = createAgentToolResultMiddlewareRunner({ runtime: "openclaw" }, [ + () => undefined, + ]); const result = await runner.applyToolResultMiddleware({ toolCallId: "call-1", @@ -476,7 +478,9 @@ describe("createAgentToolResultMiddlewareRunner", () => { }); it("collapses oversized incoming details to a truncation marker", async () => { - const runner = createAgentToolResultMiddlewareRunner({ runtime: "pi" }, [() => undefined]); + const runner = createAgentToolResultMiddlewareRunner({ runtime: "openclaw" }, [ + () => undefined, + ]); const result = await runner.applyToolResultMiddleware({ toolCallId: "call-1", diff --git a/src/agents/harness/tool-result-middleware.ts b/src/agents/harness/tool-result-middleware.ts index 113c044e7d3..e181a5e9563 100644 --- a/src/agents/harness/tool-result-middleware.ts +++ b/src/agents/harness/tool-result-middleware.ts @@ -440,7 +440,7 @@ export function createAgentToolResultMiddlewareRunner( for (const handler of handlersForRun) { try { const next = await handler({ ...event, result: current }, middlewareContext); - // Middleware may mutate event.result in place for legacy Pi parity. + // Middleware may mutate event.result in place for legacy runtime parity. // Validate the current object after every handler so in-place writes // cannot bypass the same shape and size bounds as returned results. const candidate = next?.result ?? current; diff --git a/src/agents/harness/types.ts b/src/agents/harness/types.ts index 934a6aa9001..ce0bc4ca7c3 100644 --- a/src/agents/harness/types.ts +++ b/src/agents/harness/types.ts @@ -1,7 +1,7 @@ export type AgentHarnessSupportContext = { provider: string; modelId?: string; - requestedRuntime: import("../pi-embedded-runner/runtime.js").EmbeddedAgentRuntime; + requestedRuntime: import("../agent-runtime-id.js").EmbeddedAgentRuntime; }; export type AgentHarnessSupport = @@ -9,15 +9,15 @@ export type AgentHarnessSupport = | { supported: false; reason?: string }; export type AgentHarnessAttemptParams = - import("../pi-embedded-runner/run/types.js").EmbeddedRunAttemptParams; + import("../embedded-agent-runner/run/types.js").EmbeddedRunAttemptParams; export type AgentHarnessAttemptResult = - import("../pi-embedded-runner/run/types.js").EmbeddedRunAttemptResult; + import("../embedded-agent-runner/run/types.js").EmbeddedRunAttemptResult; export type AgentHarnessSideQuestionParams = { cfg: import("../../config/types.openclaw.js").OpenClawConfig; agentDir: string; provider: string; model: string; - runtimeModel?: import("@earendil-works/pi-ai").Model; + runtimeModel?: import("openclaw/plugin-sdk/llm").Model; question: string; sessionEntry: import("../../config/sessions.js").SessionEntry; sessionStore?: Record; @@ -25,7 +25,7 @@ export type AgentHarnessSideQuestionParams = { storePath?: string; resolvedThinkLevel?: import("../../auto-reply/thinking.js").ThinkLevel; resolvedReasoningLevel: import("../../auto-reply/thinking.js").ReasoningLevel; - blockReplyChunking?: import("../pi-embedded-block-chunker.js").BlockReplyChunking; + blockReplyChunking?: import("../embedded-agent-block-chunker.js").BlockReplyChunking; resolvedBlockStreamingBreak?: "text_end" | "message_end"; opts?: import("../../auto-reply/get-reply-options.types.js").GetReplyOptions; isNewSession: boolean; @@ -43,9 +43,9 @@ export type AgentHarnessSideQuestionResult = { text: string; }; export type AgentHarnessCompactParams = - import("../pi-embedded-runner/compact.types.js").CompactEmbeddedPiSessionParams; + import("../embedded-agent-runner/compact.types.js").CompactEmbeddedAgentSessionParams; export type AgentHarnessCompactResult = - import("../pi-embedded-runner/types.js").EmbeddedPiCompactResult; + import("../embedded-agent-runner/types.js").EmbeddedAgentCompactResult; export type AgentHarnessResetParams = { sessionId?: string; sessionKey?: string; diff --git a/src/agents/harness/v2.test.ts b/src/agents/harness/v2.test.ts index 8bcc6f872db..4d0e43862c1 100644 --- a/src/agents/harness/v2.test.ts +++ b/src/agents/harness/v2.test.ts @@ -1,6 +1,6 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { PI_EMBEDDED_CONTEXT_ENGINE_HOST } from "../../context-engine/host-compat.js"; +import { OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST } from "../../context-engine/host-compat.js"; import type { ContextEngine } from "../../context-engine/types.js"; import { onInternalDiagnosticEvent, @@ -8,8 +8,8 @@ import { type DiagnosticEventMetadata, type DiagnosticEventPayload, } from "../../infra/diagnostic-events.js"; -import type { EmbeddedRunAttemptResult } from "../pi-embedded-runner/run/types.js"; -import { createPiAgentHarness } from "./builtin-pi.js"; +import type { EmbeddedRunAttemptResult } from "../embedded-agent-runner/run/types.js"; +import { createOpenClawAgentHarness } from "./builtin-openclaw.js"; import type { AgentHarness, AgentHarnessAttemptParams } from "./types.js"; import type { AgentHarnessV2 } from "./v2.js"; import { adaptAgentHarnessToV2, runAgentHarnessV2LifecycleAttempt } from "./v2.js"; @@ -25,7 +25,7 @@ function createAttemptParams(): AgentHarnessAttemptParams { timeoutMs: 5_000, provider: "codex", modelId: "gpt-5.4", - model: { id: "gpt-5.4", provider: "codex" } as Model, + model: { id: "gpt-5.4", provider: "codex" } as Model, authStorage: {} as never, authProfileStore: { version: 1, profiles: {} }, modelRegistry: {} as never, @@ -213,11 +213,11 @@ describe("AgentHarness V2 compatibility adapter", () => { expect(runAttempt).toHaveBeenCalledOnce(); }); - it("advertises Pi embedded host capabilities through the V1 adapter", async () => { - const harness = createPiAgentHarness(); + it("advertises OpenClaw embedded host capabilities through the V1 adapter", async () => { + const harness = createOpenClawAgentHarness(); expect(harness.contextEngineHostCapabilities).toEqual( - PI_EMBEDDED_CONTEXT_ENGINE_HOST.capabilities, + OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST.capabilities, ); }); diff --git a/src/agents/live-cache-regression-runner.ts b/src/agents/live-cache-regression-runner.ts index 0896f7f4ba7..1d3278dc22a 100644 --- a/src/agents/live-cache-regression-runner.ts +++ b/src/agents/live-cache-regression-runner.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; -import type { AssistantMessage, Message, Tool } from "@earendil-works/pi-ai"; +import type { AssistantMessage, Message, Tool } from "openclaw/plugin-sdk/llm"; import { Type } from "typebox"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { diff --git a/src/agents/live-cache-test-support.ts b/src/agents/live-cache-test-support.ts index 5d0a30c5f26..9a95a2bd535 100644 --- a/src/agents/live-cache-test-support.ts +++ b/src/agents/live-cache-test-support.ts @@ -4,17 +4,16 @@ import { type Api, type AssistantMessage, type Model, -} from "@earendil-works/pi-ai"; +} from "openclaw/plugin-sdk/llm"; import { getRuntimeConfig } from "../config/config.js"; import { isTruthyEnvValue } from "../infra/env.js"; -import { normalizeStringEntries } from "../shared/string-normalization.js"; +import { discoverAuthStorage, discoverModels } from "./agent-model-discovery.js"; import { resolveDefaultAgentDir } from "./agent-scope.js"; import { collectProviderApiKeys } from "./live-auth-keys.js"; import { isLiveTestEnabled } from "./live-test-helpers.js"; import { getApiKeyForModel, requireApiKey } from "./model-auth.js"; import { normalizeProviderId, parseModelRef } from "./model-selection.js"; import { ensureOpenClawModelsJson } from "./models-config.js"; -import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js"; import { buildAssistantMessageWithZeroUsage } from "./stream-message-shared.js"; export const LIVE_CACHE_TEST_ENABLED = @@ -25,7 +24,7 @@ const DEFAULT_TIMEOUT_MS = 90_000; export type LiveResolvedModel = { apiKey: string; - model: Model; + model: Model; }; export type LiveResolvedModelPool = { @@ -129,14 +128,16 @@ export function buildStableCachePrefix(tag: string, sections = 160): string { } export function extractAssistantText(message: AssistantMessage): string { - return normalizeStringEntries( - message.content.filter((block) => block.type === "text").map((block) => block.text), - ).join(" "); + return message.content + .filter((block) => block.type === "text") + .map((block) => block.text.trim()) + .filter(Boolean) + .join(" "); } export function buildAssistantHistoryTurn( text: string, - model?: Pick, "api" | "provider" | "id">, + model?: Pick, ): AssistantMessage { return buildAssistantMessageWithZeroUsage({ model: { @@ -210,7 +211,7 @@ export async function resolveLiveDirectModelPool(params: { (model) => normalizeProviderId(model.provider) === params.provider && model.api === params.api, ); - let resolvedModel: Model | undefined; + let resolvedModel: Model | undefined; if (parsed) { resolvedModel = candidates.find( (model) => diff --git a/src/agents/live-model-switch.test.ts b/src/agents/live-model-switch.test.ts index ad0d96d3acc..9ddf6d46270 100644 --- a/src/agents/live-model-switch.test.ts +++ b/src/agents/live-model-switch.test.ts @@ -1,7 +1,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const state = vi.hoisted(() => ({ - abortEmbeddedPiRunMock: vi.fn(), + abortEmbeddedAgentRunMock: vi.fn(), requestEmbeddedRunModelSwitchMock: vi.fn(), consumeEmbeddedRunModelSwitchMock: vi.fn(), resolveDefaultModelForAgentMock: vi.fn(), @@ -9,16 +9,16 @@ const state = vi.hoisted(() => ({ loadSessionStoreMock: vi.fn(), resolveStorePathMock: vi.fn(), updateSessionStoreMock: vi.fn(), - piEmbeddedModuleImported: false, + embeddedAgentModuleImported: false, })); -vi.mock("./pi-embedded.js", () => { - state.piEmbeddedModuleImported = true; +vi.mock("./embedded-agent.js", () => { + state.embeddedAgentModuleImported = true; return {}; }); -vi.mock("./pi-embedded-runner/runs.js", () => ({ - abortEmbeddedPiRun: (...args: unknown[]) => state.abortEmbeddedPiRunMock(...args), +vi.mock("./embedded-agent-runner/runs.js", () => ({ + abortEmbeddedAgentRun: (...args: unknown[]) => state.abortEmbeddedAgentRunMock(...args), requestEmbeddedRunModelSwitch: (...args: unknown[]) => state.requestEmbeddedRunModelSwitchMock(...args), consumeEmbeddedRunModelSwitch: (...args: unknown[]) => @@ -81,10 +81,10 @@ describe("live model switch", () => { }); beforeEach(() => { - state.abortEmbeddedPiRunMock.mockReset().mockReturnValue(false); + state.abortEmbeddedAgentRunMock.mockReset().mockReturnValue(false); state.requestEmbeddedRunModelSwitchMock.mockReset(); state.consumeEmbeddedRunModelSwitchMock.mockReset(); - state.piEmbeddedModuleImported = false; + state.embeddedAgentModuleImported = false; state.resolveDefaultModelForAgentMock .mockReset() .mockReturnValue({ provider: "anthropic", model: "claude-opus-4-6" }); @@ -337,7 +337,7 @@ describe("live model switch", () => { }); it("queues a live switch only when an active run was aborted", async () => { - state.abortEmbeddedPiRunMock.mockReturnValue(true); + state.abortEmbeddedAgentRunMock.mockReturnValue(true); const { requestLiveSessionModelSwitch } = await loadModule(); @@ -347,7 +347,7 @@ describe("live model switch", () => { selection: { provider: "openai", model: "gpt-5.4", authProfileId: "profile-gpt" }, }), ).toBe(true); - expect(state.abortEmbeddedPiRunMock).toHaveBeenCalledWith("session-1"); + expect(state.abortEmbeddedAgentRunMock).toHaveBeenCalledWith("session-1"); expect(state.requestEmbeddedRunModelSwitchMock).toHaveBeenCalledWith("session-1", { provider: "openai", model: "gpt-5.4", @@ -355,10 +355,10 @@ describe("live model switch", () => { }); }); - it("does not import the broad pi-embedded barrel on module load", async () => { + it("does not import the broad embedded-agent barrel on module load", async () => { await loadModule(); - expect(state.piEmbeddedModuleImported).toBe(false); + expect(state.embeddedAgentModuleImported).toBe(false); }); it("treats auth-profile-source changes as no-op when no auth profile is selected", async () => { diff --git a/src/agents/live-model-switch.ts b/src/agents/live-model-switch.ts index 1062ddacd4e..d515bb2fc64 100644 --- a/src/agents/live-model-switch.ts +++ b/src/agents/live-model-switch.ts @@ -1,17 +1,17 @@ import { resolveStorePath } from "../config/sessions/paths.js"; import { loadSessionStore, updateSessionStore } from "../config/sessions/store.js"; import type { SessionEntry } from "../config/sessions/types.js"; +import { + abortEmbeddedAgentRun, + consumeEmbeddedRunModelSwitch, + requestEmbeddedRunModelSwitch, + type EmbeddedRunModelSwitchRequest, +} from "./embedded-agent-runner/runs.js"; import { normalizeStoredOverrideModel, resolveDefaultModelForAgent, resolvePersistedSelectedModelRef, } from "./model-selection.js"; -import { - abortEmbeddedPiRun, - consumeEmbeddedRunModelSwitch, - requestEmbeddedRunModelSwitch, - type EmbeddedRunModelSwitchRequest, -} from "./pi-embedded-runner/runs.js"; export { LiveSessionModelSwitchError } from "./live-model-switch-error.js"; export type LiveSessionModelSelection = EmbeddedRunModelSwitchRequest; import { normalizeOptionalString } from "../shared/string-coerce.js"; @@ -72,7 +72,7 @@ export function requestLiveSessionModelSwitch(params: { if (!sessionId) { return false; } - const aborted = abortEmbeddedPiRun(sessionId); + const aborted = abortEmbeddedAgentRun(sessionId); if (!aborted) { return false; } diff --git a/src/agents/live-model-turn-probes.ts b/src/agents/live-model-turn-probes.ts index 2ae4458ebec..1aa8361df1c 100644 --- a/src/agents/live-model-turn-probes.ts +++ b/src/agents/live-model-turn-probes.ts @@ -1,5 +1,4 @@ -import type { Api, AssistantMessage, Context, Model } from "@earendil-works/pi-ai"; -import { normalizeStringEntries } from "../shared/string-normalization.js"; +import type { AssistantMessage, Context, Model } from "openclaw/plugin-sdk/llm"; export const LIVE_MODEL_FILE_PROBE_TOKEN = "opal"; @@ -45,7 +44,7 @@ const KNOWN_EMPTY_IMAGE_PROBE_MODELS = new Set([ "openrouter/bytedance-seed/seed-1.6", ]); -function modelKey(model: Pick, "id" | "provider">): string { +function modelKey(model: Pick): string { return `${model.provider}/${model.id}`; } @@ -61,29 +60,29 @@ export function isLiveModelProbeEnabled( } export function extractAssistantText(message: Pick): string { - return normalizeStringEntries( - message.content.filter((block) => block.type === "text").map((block) => block.text), - ).join(" "); + return message.content + .filter((block) => block.type === "text") + .map((block) => block.text.trim()) + .filter(Boolean) + .join(" "); } -export function modelSupportsImageInput(model: Pick, "input">): boolean { +export function modelSupportsImageInput(model: Pick): boolean { return model.input.includes("image"); } -export function shouldSkipLiveModelExtraProbes( - model: Pick, "id" | "provider">, -): boolean { +export function shouldSkipLiveModelExtraProbes(model: Pick): boolean { return KNOWN_EMPTY_EXTRA_PROBE_MODELS.has(modelKey(model)); } -export function shouldSkipLiveModelFileProbe(model: Pick, "id" | "provider">): boolean { +export function shouldSkipLiveModelFileProbe(model: Pick): boolean { if (model.provider === "opencode-go") { return true; } return KNOWN_EMPTY_FILE_PROBE_MODELS.has(modelKey(model)); } -export function shouldSkipLiveModelImageProbe(model: Pick, "id" | "provider">): boolean { +export function shouldSkipLiveModelImageProbe(model: Pick): boolean { return KNOWN_EMPTY_IMAGE_PROBE_MODELS.has(modelKey(model)); } diff --git a/src/agents/live-test-helpers.ts b/src/agents/live-test-helpers.ts index 260819b23da..658d22dd4b6 100644 --- a/src/agents/live-test-helpers.ts +++ b/src/agents/live-test-helpers.ts @@ -1,4 +1,4 @@ -import { completeSimple, type Api, type Model } from "@earendil-works/pi-ai"; +import { completeSimple, type Api, type Model } from "openclaw/plugin-sdk/llm"; import { isTruthyEnvValue } from "../infra/env.js"; const LIVE_OK_PROMPT = "Reply with the word ok."; diff --git a/src/agents/live-test-provider-drift.ts b/src/agents/live-test-provider-drift.ts index 9e7300f3935..dd0553ed195 100644 --- a/src/agents/live-test-provider-drift.ts +++ b/src/agents/live-test-provider-drift.ts @@ -1,13 +1,13 @@ import { isCloudflareOrHtmlErrorPage } from "../shared/assistant-error-format.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; -import { isAnthropicBillingError, isApiKeyRateLimitError } from "./live-auth-keys.js"; -import { isModelNotFoundErrorMessage } from "./live-model-errors.js"; import { isAuthErrorMessage, isBillingErrorMessage, isRateLimitErrorMessage, isTimeoutErrorMessage, -} from "./pi-embedded-helpers/failover-matches.js"; +} from "./embedded-agent-helpers/failover-matches.js"; +import { isAnthropicBillingError, isApiKeyRateLimitError } from "./live-auth-keys.js"; +import { isModelNotFoundErrorMessage } from "./live-model-errors.js"; export type LiveProviderDriftReason = | "auth" diff --git a/src/agents/local-model-lean.test.ts b/src/agents/local-model-lean.test.ts index af70a5644f1..d552a448399 100644 --- a/src/agents/local-model-lean.test.ts +++ b/src/agents/local-model-lean.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; import { filterLocalModelLeanTools, isLocalModelLeanEnabled } from "./local-model-lean.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; function tools(names: string[]): AnyAgentTool[] { return names.map((name) => ({ name })) as AnyAgentTool[]; diff --git a/src/agents/local-model-lean.ts b/src/agents/local-model-lean.ts index a898c45eada..837c0600038 100644 --- a/src/agents/local-model-lean.ts +++ b/src/agents/local-model-lean.ts @@ -1,7 +1,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js"; import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope-config.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; const LOCAL_MODEL_LEAN_DENY_TOOL_NAMES = new Set(["browser", "cron", "message"]); diff --git a/src/agents/minimax.live.test.ts b/src/agents/minimax.live.test.ts index 37ef3be1434..fdc64816f42 100644 --- a/src/agents/minimax.live.test.ts +++ b/src/agents/minimax.live.test.ts @@ -1,4 +1,4 @@ -import { completeSimple, type Model } from "@earendil-works/pi-ai"; +import { completeSimple, type Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { createSingleUserPromptMessage, diff --git a/src/agents/model-auth.profiles.test.ts b/src/agents/model-auth.profiles.test.ts index 7cbdd3fd709..8ac9278b6bd 100644 --- a/src/agents/model-auth.profiles.test.ts +++ b/src/agents/model-auth.profiles.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { withEnvAsync } from "../test-utils/env.js"; @@ -44,7 +44,7 @@ async function expectVertexAdcEnvApiKey(params: { } } -function testModelDefinition(id: string): Model { +function testModelDefinition(id: string): Model { return { id, name: id, @@ -415,7 +415,7 @@ describe("getApiKeyForModel", () => { id: "codex-mini-latest", provider: "openai-codex", api: "openai-codex-responses", - } as Model; + } as Model; const store = ensureAuthProfileStore(process.env.OPENCLAW_AGENT_DIR, { allowKeychainPrompt: false, diff --git a/src/agents/model-auth.test.ts b/src/agents/model-auth.test.ts index 49425dbc7e2..4eefb169806 100644 --- a/src/agents/model-auth.test.ts +++ b/src/agents/model-auth.test.ts @@ -1,4 +1,4 @@ -import type { Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ModelProviderConfig } from "../config/config.js"; import type { AuthProfileStore } from "./auth-profiles.js"; @@ -338,15 +338,15 @@ describe("resolveModelAuthMode", () => { ).toBe("aws-sdk"); }); - it("returns aws-sdk for bedrock alias without explicit auth override", () => { + it("does not infer aws-sdk for bedrock alias without explicit auth override", () => { expect(resolveModelAuthMode("bedrock", undefined, { version: 1, profiles: {} })).toBe( - "aws-sdk", + "unknown", ); }); - it("returns aws-sdk for aws-bedrock alias without explicit auth override", () => { + it("does not infer aws-sdk for aws-bedrock alias without explicit auth override", () => { expect(resolveModelAuthMode("aws-bedrock", undefined, { version: 1, profiles: {} })).toBe( - "aws-sdk", + "unknown", ); }); @@ -1315,7 +1315,7 @@ describe("resolveApiKeyForProvider – synthetic local auth for custom providers ).rejects.toThrow('No API key found for provider "custom"'); }); - it("keeps built-in aws-sdk fallback for local baseUrl overrides", async () => { + it("uses explicit aws-sdk auth for local baseUrl overrides", async () => { const auth = await resolveApiKeyForProvider({ provider: "amazon-bedrock", cfg: { @@ -1324,6 +1324,7 @@ describe("resolveApiKeyForProvider – synthetic local auth for custom providers "amazon-bedrock": { baseUrl: "http://127.0.0.1:8080/v1", models: [], + auth: "aws-sdk", }, }, }, @@ -1333,6 +1334,27 @@ describe("resolveApiKeyForProvider – synthetic local auth for custom providers expect(auth.mode).toBe("aws-sdk"); expect(auth.apiKey).toBeUndefined(); }); + + it("uses implicit aws-sdk auth for built-in Bedrock Converse models", async () => { + const auth = await getApiKeyForModel({ + model: { + id: "us.anthropic.claude-sonnet-4-6-v1", + name: "Claude Sonnet", + provider: "amazon-bedrock", + api: "bedrock-converse-stream", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, + maxTokens: 8192, + }, + store: { version: 1, profiles: {} }, + }); + + expect(auth.mode).toBe("aws-sdk"); + expect(auth.apiKey).toBeUndefined(); + }); }); describe("applyLocalNoAuthHeaderOverride", () => { diff --git a/src/agents/model-catalog.test.ts b/src/agents/model-catalog.test.ts index fa7248610a5..a7f24654680 100644 --- a/src/agents/model-catalog.test.ts +++ b/src/agents/model-catalog.test.ts @@ -2,7 +2,7 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } import type { OpenClawConfig } from "../config/config.js"; import { resetLogger, setLoggerOverride } from "../logging/logger.js"; -type PiSdkModule = typeof import("./pi-model-discovery.js"); +type AgentModelDiscoveryModule = typeof import("./agent-model-discovery.js"); let setModelCatalogImportForTest: typeof import("./model-catalog.js").setModelCatalogImportForTest; let findModelCatalogEntry: typeof import("./model-catalog.js").findModelCatalogEntry; @@ -47,33 +47,43 @@ function mockCatalogImportFailThenRecover() { return { discoverAuthStorage: () => ({}), AuthStorage: function AuthStorage() {}, + discoverModels: () => ({ + getAll() { + return [{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }]; + }, + }), ModelRegistry: class { getAll() { return [{ id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }]; } }, - } as unknown as PiSdkModule; + } as unknown as AgentModelDiscoveryModule; }); return () => call; } -function mockPiDiscoveryModels(models: unknown[]) { +function mockAgentDiscoveryModels(models: unknown[]) { setModelCatalogImportForTest( async () => ({ discoverAuthStorage: () => ({}), AuthStorage: function AuthStorage() {}, + discoverModels: () => ({ + getAll() { + return models; + }, + }), ModelRegistry: class { getAll() { return models; } }, - }) as unknown as PiSdkModule, + }) as unknown as AgentModelDiscoveryModule, ); } function mockSingleOpenAiCatalogModel() { - mockPiDiscoveryModels([{ id: "gpt-4.1", provider: "openai", name: "GPT-4.1" }]); + mockAgentDiscoveryModels([{ id: "gpt-4.1", provider: "openai", name: "GPT-4.1" }]); } function emptyPluginMetadataSnapshot() { @@ -184,8 +194,8 @@ describe("loadModelCatalog", () => { vi.doMock("../plugins/provider-runtime.runtime.js", () => ({ augmentModelCatalogWithProviderPlugins: vi.fn().mockResolvedValue([]), })); - currentPluginMetadataSnapshotMock = vi.fn<(...args: unknown[]) => unknown>(); - loadPluginMetadataSnapshotMock = vi.fn<(...args: unknown[]) => unknown>(); + currentPluginMetadataSnapshotMock = vi.fn(() => emptyPluginMetadataSnapshot()); + loadPluginMetadataSnapshotMock = vi.fn(() => emptyPluginMetadataSnapshot()); vi.doMock("../plugins/current-plugin-metadata-snapshot.js", () => ({ getCurrentPluginMetadataSnapshot: currentPluginMetadataSnapshotMock, })); @@ -257,7 +267,7 @@ describe("loadModelCatalog", () => { it("reloads dynamic registry entries after clearing the cache", async () => { const models = [{ id: "existing", name: "Existing", provider: "ollama" }]; - mockPiDiscoveryModels(models); + mockAgentDiscoveryModels(models); const first = await loadModelCatalog({ config: {} as OpenClawConfig }); expect(first).toStrictEqual([ @@ -274,7 +284,7 @@ describe("loadModelCatalog", () => { models.push({ id: "glm-5.1:cloud", name: "GLM 5.1 Cloud", provider: "ollama" }); resetModelCatalogCacheForTest(); - mockPiDiscoveryModels(models); + mockAgentDiscoveryModels(models); const second = await loadModelCatalog({ config: {} as OpenClawConfig }); expect(second).toStrictEqual([ @@ -307,6 +317,20 @@ describe("loadModelCatalog", () => { ({ discoverAuthStorage: () => ({}), AuthStorage: function AuthStorage() {}, + discoverModels: () => ({ + getAll() { + return [ + { id: "gpt-4.1", name: "GPT-4.1", provider: "openai" }, + { + get id() { + throw new Error("boom"); + }, + provider: "openai", + name: "bad", + }, + ]; + }, + }), ModelRegistry: class { getAll() { return [ @@ -321,7 +345,7 @@ describe("loadModelCatalog", () => { ]; } }, - }) as unknown as PiSdkModule, + }) as unknown as AgentModelDiscoveryModule, ); const result = await loadModelCatalog({ config: {} as OpenClawConfig }); @@ -333,10 +357,12 @@ describe("loadModelCatalog", () => { }); it("does not prepare models.json or import provider discovery when loading fallback catalog in read-only mode", async () => { - const importPiSdk = vi.fn(async () => { + const importAgentDiscoveryModule = vi.fn(async () => { throw new Error("provider discovery should not load"); }); - setModelCatalogImportForTest(importPiSdk as unknown as () => Promise); + setModelCatalogImportForTest( + importAgentDiscoveryModule as unknown as () => Promise, + ); currentPluginMetadataSnapshotMock.mockReturnValueOnce(undefined); loadPluginMetadataSnapshotMock.mockImplementationOnce(() => { throw new Error("metadata scan should not run"); @@ -369,7 +395,7 @@ describe("loadModelCatalog", () => { const entry = requireCatalogEntry(result, "openai", "gpt-test"); expect(entry.name).toBe("GPT Test"); expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled(); - expect(importPiSdk).not.toHaveBeenCalled(); + expect(importAgentDiscoveryModule).not.toHaveBeenCalled(); expect(loadPluginMetadataSnapshotMock).not.toHaveBeenCalled(); }); @@ -464,10 +490,12 @@ describe("loadModelCatalog", () => { }, ], }); - const importPiSdk = vi.fn(async () => { + const importAgentDiscoveryModule = vi.fn(async () => { throw new Error("provider discovery should not load"); }); - setModelCatalogImportForTest(importPiSdk as unknown as () => Promise); + setModelCatalogImportForTest( + importAgentDiscoveryModule as unknown as () => Promise, + ); const result = await loadModelCatalog({ config: {} as OpenClawConfig, readOnly: true }); @@ -481,7 +509,7 @@ describe("loadModelCatalog", () => { }, ]); expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled(); - expect(importPiSdk).not.toHaveBeenCalled(); + expect(importAgentDiscoveryModule).not.toHaveBeenCalled(); }); it("preserves registry defaults for minimal persisted read-only catalog rows", async () => { @@ -663,7 +691,7 @@ describe("loadModelCatalog", () => { it("loads manifest model id policies once for discovered catalog rows", async () => { currentPluginMetadataSnapshotMock.mockReturnValue(undefined); loadPluginMetadataSnapshotMock.mockReturnValue(modelIdNormalizationSnapshot()); - mockPiDiscoveryModels([ + mockAgentDiscoveryModels([ { provider: "custom", id: "model-a", name: "Model A" }, { provider: "custom", id: "model-b", name: "Model B" }, { provider: "custom", id: "model-c", name: "Model C" }, @@ -719,7 +747,7 @@ describe("loadModelCatalog", () => { }); it("does not synthesize stale openai-codex/gpt-5.3-codex-spark entries from gpt-5.4", async () => { - mockPiDiscoveryModels([ + mockAgentDiscoveryModels([ { id: "gpt-5.4", provider: "openai-codex", @@ -742,7 +770,7 @@ describe("loadModelCatalog", () => { }); it("filters stale gpt-5.3-codex-spark built-ins from the catalog", async () => { - mockPiDiscoveryModels([ + mockAgentDiscoveryModels([ { id: "gpt-5.3-codex-spark", provider: "openai", @@ -776,7 +804,7 @@ describe("loadModelCatalog", () => { }); it("keeps available openai-codex 5.1/5.2/5.3 built-ins in the catalog", async () => { - mockPiDiscoveryModels([ + mockAgentDiscoveryModels([ { id: "gpt-5.1-codex-mini", provider: "openai-codex", @@ -821,7 +849,7 @@ describe("loadModelCatalog", () => { }); it("does not synthesize gpt-5.4 OpenAI forward-compat entries from template models", async () => { - mockPiDiscoveryModels([ + mockAgentDiscoveryModels([ { id: "gpt-5.2", provider: "openai", @@ -1103,7 +1131,7 @@ describe("loadModelCatalog", () => { }); it("dedupes configured models against discovered provider aliases", async () => { - mockPiDiscoveryModels([{ id: "glm-5", provider: "z.ai", name: "GLM-5" }]); + mockAgentDiscoveryModels([{ id: "glm-5", provider: "z.ai", name: "GLM-5" }]); const result = await loadModelCatalog({ config: { @@ -1147,7 +1175,7 @@ describe("loadModelCatalog", () => { }); it("does not duplicate provider-owned supplemental models already present in ModelRegistry", async () => { - mockPiDiscoveryModels([ + mockAgentDiscoveryModels([ { id: "kilo/auto", provider: "kilocode", diff --git a/src/agents/model-compat.test.ts b/src/agents/model-compat.test.ts index 7d0f54370f6..14e5cb1af57 100644 --- a/src/agents/model-compat.test.ts +++ b/src/agents/model-compat.test.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { beforeEach, describe, expect, it, vi } from "vitest"; const providerRuntimeMocks = vi.hoisted(() => ({ @@ -22,7 +22,7 @@ import { selectHighSignalLiveItems, } from "./live-model-filter.js"; -const baseModel = (): Model => +const baseModel = (): Model => ({ id: "glm-4.7", name: "GLM-4.7", @@ -34,46 +34,46 @@ const baseModel = (): Model => cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 8192, maxTokens: 1024, - }) as Model; + }) as Model; -function supportsDeveloperRole(model: Model): boolean | undefined { +function supportsDeveloperRole(model: Model): boolean | undefined { return (model.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole; } -function supportsUsageInStreaming(model: Model): boolean | undefined { +function supportsUsageInStreaming(model: Model): boolean | undefined { return (model.compat as { supportsUsageInStreaming?: boolean } | undefined) ?.supportsUsageInStreaming; } -function supportsStrictMode(model: Model): boolean | undefined { +function supportsStrictMode(model: Model): boolean | undefined { return (model.compat as { supportsStrictMode?: boolean } | undefined)?.supportsStrictMode; } -function expectSupportsDeveloperRoleForcedOff(overrides?: Partial>): void { +function expectSupportsDeveloperRoleForcedOff(overrides?: Partial): void { const model = { ...baseModel(), ...overrides }; delete (model as { compat?: unknown }).compat; - const normalized = normalizeModelCompat(model as Model); + const normalized = normalizeModelCompat(model as Model); expect(supportsDeveloperRole(normalized)).toBe(false); } -function expectSupportsUsageInStreamingForcedOff(overrides?: Partial>): void { +function expectSupportsUsageInStreamingForcedOff(overrides?: Partial): void { const model = { ...baseModel(), ...overrides }; delete (model as { compat?: unknown }).compat; - const normalized = normalizeModelCompat(model as Model); + const normalized = normalizeModelCompat(model as Model); expect(supportsUsageInStreaming(normalized)).toBe(false); } -function expectSupportsStrictModeForcedOff(overrides?: Partial>): void { +function expectSupportsStrictModeForcedOff(overrides?: Partial): void { const model = { ...baseModel(), ...overrides }; delete (model as { compat?: unknown }).compat; - const normalized = normalizeModelCompat(model as Model); + const normalized = normalizeModelCompat(model as Model); expect(supportsStrictMode(normalized)).toBe(false); } -function expectNativeStreamingSupported(overrides: Partial>): void { +function expectNativeStreamingSupported(overrides: Partial): void { const model = { ...baseModel(), ...overrides }; delete (model as { compat?: unknown }).compat; - const normalized = normalizeModelCompat(model as Model); + const normalized = normalizeModelCompat(model as Model); expect(supportsDeveloperRole(normalized)).toBe(false); expect(supportsUsageInStreaming(normalized)).toBe(true); expect(supportsStrictMode(normalized)).toBe(false); @@ -85,7 +85,7 @@ beforeEach(() => { }); describe("normalizeModelCompat — Anthropic baseUrl", () => { - const anthropicBase = (): Model => + const anthropicBase = (): Model => ({ id: "claude-opus-4-6", name: "claude-opus-4-6", @@ -96,7 +96,7 @@ describe("normalizeModelCompat — Anthropic baseUrl", () => { cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 200_000, maxTokens: 8_192, - }) as Model; + }) as Model; it("strips /v1 suffix from anthropic-messages baseUrl", () => { const model = { ...anthropicBase(), baseUrl: "https://api.anthropic.com/v1" }; @@ -253,7 +253,7 @@ describe("normalizeModelCompat", () => { }; delete (model as { baseUrl?: unknown }).baseUrl; delete (model as { compat?: unknown }).compat; - const normalized = normalizeModelCompat(model as Model); + const normalized = normalizeModelCompat(model as Model); expect(normalized.compat).toBeUndefined(); }); diff --git a/src/agents/model-fallback-observation.ts b/src/agents/model-fallback-observation.ts index 35c8a9d5a95..a882445f115 100644 --- a/src/agents/model-fallback-observation.ts +++ b/src/agents/model-fallback-observation.ts @@ -1,8 +1,8 @@ import { createSubsystemLogger } from "../logging/subsystem.js"; import { sanitizeForLog } from "../terminal/ansi.js"; +import { buildTextObservationFields } from "./embedded-agent-error-observation.js"; +import type { FailoverReason } from "./embedded-agent-helpers.js"; import type { FallbackAttempt, ModelCandidate } from "./model-fallback.types.js"; -import { buildTextObservationFields } from "./pi-embedded-error-observation.js"; -import type { FailoverReason } from "./pi-embedded-helpers.js"; const decisionLog = createSubsystemLogger("model-fallback").child("decision"); diff --git a/src/agents/model-fallback.run-embedded.e2e.test.ts b/src/agents/model-fallback.run-embedded.e2e.test.ts index 28158b20e6f..66c9bc842ad 100644 --- a/src/agents/model-fallback.run-embedded.e2e.test.ts +++ b/src/agents/model-fallback.run-embedded.e2e.test.ts @@ -4,19 +4,19 @@ import path from "node:path"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import type { AuthProfileFailureReason } from "./auth-profiles.js"; +import { classifyEmbeddedAgentRunResultForModelFallback } from "./embedded-agent-runner/result-fallback-classifier.js"; +import type { EmbeddedRunAttemptResult } from "./embedded-agent-runner/run/types.js"; import { runWithModelFallback } from "./model-fallback.js"; -import { classifyEmbeddedPiRunResultForModelFallback } from "./pi-embedded-runner/result-fallback-classifier.js"; -import type { EmbeddedRunAttemptResult } from "./pi-embedded-runner/run/types.js"; import { buildEmbeddedRunnerAssistant, createResolvedEmbeddedRunnerModel, makeEmbeddedRunnerAttempt, -} from "./test-helpers/pi-embedded-runner-e2e-fixtures.js"; +} from "./test-helpers/embedded-agent-runner-e2e-fixtures.js"; import { installEmbeddedRunnerBackoffE2eMocks, installEmbeddedRunnerBaseE2eMocks, installEmbeddedRunnerFastRunE2eMocks, -} from "./test-helpers/pi-embedded-runner-e2e-mocks.js"; +} from "./test-helpers/embedded-agent-runner-e2e-mocks.js"; const runEmbeddedAttemptMock = vi.fn<(params: unknown) => Promise>(); const { computeBackoffMock, sleepWithAbortMock } = vi.hoisted(() => ({ @@ -46,18 +46,18 @@ const installRunEmbeddedMocks = () => { computeBackoff: (policy, attempt) => computeBackoffMock(policy, attempt), sleepWithAbort: (ms, abortSignal) => sleepWithAbortMock(ms, abortSignal), }); - vi.doMock("./pi-embedded-runner/model.js", () => ({ + vi.doMock("./embedded-agent-runner/model.js", () => ({ resolveModelAsync: async (provider: string, modelId: string) => createResolvedEmbeddedRunnerModel(provider, modelId), })); }; -let runEmbeddedPiAgent: typeof import("./pi-embedded-runner/run.js").runEmbeddedPiAgent; +let runEmbeddedAgent: typeof import("./embedded-agent-runner/run.js").runEmbeddedAgent; beforeAll(async () => { vi.resetModules(); installRunEmbeddedMocks(); - ({ runEmbeddedPiAgent } = await import("./pi-embedded-runner/run.js")); + ({ runEmbeddedAgent } = await import("./embedded-agent-runner/run.js")); }); beforeEach(() => { @@ -237,7 +237,7 @@ async function runEmbeddedFallback(params: { runId: params.runId, agentDir: params.agentDir, run: (provider, model, options) => - runEmbeddedPiAgent({ + runEmbeddedAgent({ sessionId: `session:${params.runId}`, sessionKey: params.sessionKey, sessionFile: path.join(params.workspaceDir, `${params.runId}.jsonl`), @@ -372,7 +372,7 @@ function expectProviderAttemptCounts(expected: { openai: number; groq: number }) expect(countProviderAttempts("groq")).toBe(expected.groq); } -describe("runWithModelFallback + runEmbeddedPiAgent failover behavior", () => { +describe("runWithModelFallback + runEmbeddedAgent failover behavior", () => { it("keeps tool summary on incomplete side-effect terminal results", async () => { await withAgentWorkspace(async ({ agentDir, workspaceDir }) => { await writeAuthStore(agentDir); @@ -388,7 +388,7 @@ describe("runWithModelFallback + runEmbeddedPiAgent failover behavior", () => { }), ); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ sessionId: "session:tool-side-effect-terminal", sessionKey: "agent:test:tool-side-effect-terminal", sessionFile: path.join(workspaceDir, "tool-side-effect-terminal.jsonl"), @@ -407,7 +407,7 @@ describe("runWithModelFallback + runEmbeddedPiAgent failover behavior", () => { expect(result.meta.toolSummary?.calls).toBe(1); expect(result.meta.toolSummary?.tools).toEqual(["write"]); expect( - classifyEmbeddedPiRunResultForModelFallback({ + classifyEmbeddedAgentRunResultForModelFallback({ provider: "openai-codex", model: "gpt-5.4", result, diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index 4c93f3f6073..0a7171d26c7 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -16,6 +16,8 @@ import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot import { CommandLaneTaskTimeoutError } from "../process/command-queue.js"; import { AUTH_STORE_VERSION } from "./auth-profiles/constants.js"; import type { AuthProfileStore } from "./auth-profiles/types.js"; +import { classifyEmbeddedAgentRunResultForModelFallback } from "./embedded-agent-runner/result-fallback-classifier.js"; +import type { EmbeddedAgentRunResult } from "./embedded-agent-runner/types.js"; import { FailoverError } from "./failover-error.js"; import { MissingAgentHarnessError } from "./harness/errors.js"; import { LiveSessionModelSwitchError } from "./live-model-switch-error.js"; @@ -25,8 +27,6 @@ import { runWithImageModelFallback, runWithModelFallback as runWithModelFallbackBase, } from "./model-fallback.js"; -import { classifyEmbeddedPiRunResultForModelFallback } from "./pi-embedded-runner/result-fallback-classifier.js"; -import type { EmbeddedPiRunResult } from "./pi-embedded-runner/types.js"; import { SessionWriteLockTimeoutError } from "./session-write-lock-error.js"; import { makeModelFallbackCfg } from "./test-helpers/model-fallback-config-fixture.js"; @@ -1268,7 +1268,7 @@ describe("runWithModelFallback", () => { }); it("keeps tool-executing empty GPT-5 runs out of fallback", () => { - const runResult: EmbeddedPiRunResult = { + const runResult: EmbeddedAgentRunResult = { payloads: [], meta: { durationMs: 1, @@ -1280,7 +1280,7 @@ describe("runWithModelFallback", () => { }; expect( - classifyEmbeddedPiRunResultForModelFallback({ + classifyEmbeddedAgentRunResultForModelFallback({ provider: "openai-codex", model: "gpt-5.4", result: runResult, @@ -1289,7 +1289,7 @@ describe("runWithModelFallback", () => { }); it("keeps normalized silent GPT-5 terminal replies out of fallback", () => { - const runResult: EmbeddedPiRunResult = { + const runResult: EmbeddedAgentRunResult = { payloads: [], meta: { durationMs: 1, @@ -1298,7 +1298,7 @@ describe("runWithModelFallback", () => { }; expect( - classifyEmbeddedPiRunResultForModelFallback({ + classifyEmbeddedAgentRunResultForModelFallback({ provider: "openai-codex", model: "gpt-5.4", result: runResult, @@ -1307,7 +1307,7 @@ describe("runWithModelFallback", () => { }); it("keeps before_agent_run hook blocks out of empty-result fallback", () => { - const runResult: EmbeddedPiRunResult = { + const runResult: EmbeddedAgentRunResult = { payloads: [{ text: "Blocked by before-run policy.", isError: true }], meta: { durationMs: 1, @@ -1320,7 +1320,7 @@ describe("runWithModelFallback", () => { }; expect( - classifyEmbeddedPiRunResultForModelFallback({ + classifyEmbeddedAgentRunResultForModelFallback({ provider: "atlassian-ai-gateway-openai", model: "gpt-5.5-2026-04-23", result: runResult, @@ -1329,7 +1329,7 @@ describe("runWithModelFallback", () => { }); it("uses harness-owned terminal classification for GPT-5 fallback", () => { - const runResult: EmbeddedPiRunResult = { + const runResult: EmbeddedAgentRunResult = { payloads: [], meta: { durationMs: 1, @@ -1337,7 +1337,7 @@ describe("runWithModelFallback", () => { }, }; - const classification = classifyEmbeddedPiRunResultForModelFallback({ + const classification = classifyEmbeddedAgentRunResultForModelFallback({ provider: "codex", model: "gpt-5.4", result: runResult, @@ -1348,7 +1348,7 @@ describe("runWithModelFallback", () => { }); it("classifies non-GPT incomplete terminal errors for configured fallback", () => { - const runResult: EmbeddedPiRunResult = { + const runResult: EmbeddedAgentRunResult = { payloads: [ { text: "⚠️ Agent couldn't generate a response. Please try again.", isError: true }, ], @@ -1357,7 +1357,7 @@ describe("runWithModelFallback", () => { }, }; - const classification = classifyEmbeddedPiRunResultForModelFallback({ + const classification = classifyEmbeddedAgentRunResultForModelFallback({ provider: "anthropic", model: "claude-opus-4.7", result: runResult, @@ -1368,7 +1368,7 @@ describe("runWithModelFallback", () => { }); it("keeps aborted harness-classified GPT-5 runs out of fallback", () => { - const runResult: EmbeddedPiRunResult = { + const runResult: EmbeddedAgentRunResult = { payloads: [], meta: { durationMs: 1, @@ -1378,7 +1378,7 @@ describe("runWithModelFallback", () => { }; expect( - classifyEmbeddedPiRunResultForModelFallback({ + classifyEmbeddedAgentRunResultForModelFallback({ provider: "codex", model: "gpt-5.4", result: runResult, diff --git a/src/agents/model-fallback.ts b/src/agents/model-fallback.ts index 792bb65cfef..450ea86bf9b 100644 --- a/src/agents/model-fallback.ts +++ b/src/agents/model-fallback.ts @@ -17,10 +17,14 @@ import { isCommandLaneTaskTimeoutError } from "../process/command-queue.js"; import { createLazyImportLoader } from "../shared/lazy-promise.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { sanitizeForLog } from "../terminal/ansi.js"; +import { isDefaultAgentRuntimeId } from "./agent-runtime-id.js"; +import { normalizeOptionalAgentRuntimeId } from "./agent-runtime-id.js"; import { externalCliDiscoveryForProviders } from "./auth-profiles/external-cli-discovery.js"; import { hasAnyAuthProfileStoreSource } from "./auth-profiles/source-check.js"; import type { AuthProfileStore } from "./auth-profiles/types.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js"; +import { isLikelyContextOverflowError } from "./embedded-agent-helpers/errors.js"; +import type { FailoverReason } from "./embedded-agent-helpers/types.js"; import { FailoverError, coerceToFailoverError, @@ -58,8 +62,6 @@ import { resolveConfiguredModelRef, resolveModelRefFromString, } from "./model-selection-resolve.js"; -import { isLikelyContextOverflowError } from "./pi-embedded-helpers/errors.js"; -import type { FailoverReason } from "./pi-embedded-helpers/types.js"; import { resolveSessionSuspensionReason, suspendSession } from "./session-suspension.js"; const log = createSubsystemLogger("model-fallback"); @@ -396,7 +398,7 @@ async function assertModelFallbackCandidateHarnessAvailable( if (isCliProvider(params.provider, params.cfg)) { return; } - const agentRuntimeOverride = normalizeOptionalString(agentHarnessRuntimeOverride); + const agentRuntimeOverride = normalizeOptionalAgentRuntimeId(agentHarnessRuntimeOverride); const harnessPolicy = resolveAgentHarnessPolicy({ provider: params.provider, modelId: params.model, @@ -404,14 +406,20 @@ async function assertModelFallbackCandidateHarnessAvailable( agentId: params.agentId, sessionKey: params.sessionKey, }); - const agentRuntime = agentRuntimeOverride ?? harnessPolicy.runtime; - const agentRuntimeSource = agentRuntimeOverride ? "model" : harnessPolicy.runtimeSource; + const agentRuntime = + agentRuntimeOverride && !isDefaultAgentRuntimeId(agentRuntimeOverride) + ? agentRuntimeOverride + : harnessPolicy.runtime; + const agentRuntimeSource = + agentRuntimeOverride && !isDefaultAgentRuntimeId(agentRuntimeOverride) + ? "model" + : harnessPolicy.runtimeSource; if (isCliAgentRuntime(agentRuntime, params.cfg)) { return; } if ( agentRuntime === "auto" || - agentRuntime === "pi" || + agentRuntime === "openclaw" || (agentRuntime === "codex" && agentRuntimeSource === "implicit") ) { return; @@ -421,7 +429,12 @@ async function assertModelFallbackCandidateHarnessAvailable( model: params.model, agentHarnessRuntimeOverride, }); - if (!getRegisteredAgentHarness(agentRuntime)) { + if ( + agentRuntime !== "auto" && + agentRuntime !== "openclaw" && + !(agentRuntime === "codex" && agentRuntimeSource === "implicit") && + !getRegisteredAgentHarness(agentRuntime) + ) { throw new MissingAgentHarnessError(agentRuntime); } } diff --git a/src/agents/model-fallback.types.ts b/src/agents/model-fallback.types.ts index ef2ed1cc015..a160f81355e 100644 --- a/src/agents/model-fallback.types.ts +++ b/src/agents/model-fallback.types.ts @@ -1,4 +1,4 @@ -import type { FailoverReason } from "./pi-embedded-helpers/types.js"; +import type { FailoverReason } from "./embedded-agent-helpers/types.js"; export type ModelCandidate = { provider: string; diff --git a/src/agents/model-registry-loader.ts b/src/agents/model-registry-loader.ts new file mode 100644 index 00000000000..03cf749eadc --- /dev/null +++ b/src/agents/model-registry-loader.ts @@ -0,0 +1,30 @@ +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { discoverAuthStorage, discoverModels } from "./agent-model-discovery.js"; +import { resolveDefaultAgentDir } from "./agent-scope.js"; +import type { ModelRegistry } from "./sessions/index.js"; + +export type LoadAgentModelRegistryOptions = { + providerFilter?: string; + normalizeModels?: boolean; + readOnly?: boolean; + skipCredentials?: boolean; + workspaceDir?: string; +}; + +export function loadAgentModelRegistry( + config: OpenClawConfig, + options: LoadAgentModelRegistryOptions = {}, +): { agentDir: string; registry: ModelRegistry } { + const agentDir = resolveDefaultAgentDir(config); + const authStorage = discoverAuthStorage(agentDir, { + readOnly: options.readOnly ?? true, + skipCredentials: options.skipCredentials, + config, + workspaceDir: options.workspaceDir, + }); + const registry = discoverModels(authStorage, agentDir, { + providerFilter: options.providerFilter, + normalizeModels: options.normalizeModels, + }); + return { agentDir, registry }; +} diff --git a/src/agents/model-runtime-aliases.test.ts b/src/agents/model-runtime-aliases.test.ts index 56f57b9da25..82e912ec611 100644 --- a/src/agents/model-runtime-aliases.test.ts +++ b/src/agents/model-runtime-aliases.test.ts @@ -67,13 +67,13 @@ describe("resolveCliRuntimeExecutionProvider", () => { ).toBe("claude-cli"); }); - it("does not override an explicit PI model-runtime policy with CLI auth", () => { + it("does not override an explicit OpenClaw model-runtime policy with CLI auth", () => { expect( resolveCliRuntimeExecutionProvider({ cfg: createAnthropicAuthConfig({ order: ["anthropic:claude-cli"], models: { - "anthropic/opus-4.7": { agentRuntime: { id: "pi" } }, + "anthropic/opus-4.7": { agentRuntime: { id: "openclaw" } }, }, }), provider: "anthropic", diff --git a/src/agents/model-runtime-aliases.ts b/src/agents/model-runtime-aliases.ts index 451a8a2eb17..d8645e3df6c 100644 --- a/src/agents/model-runtime-aliases.ts +++ b/src/agents/model-runtime-aliases.ts @@ -303,7 +303,7 @@ export function resolveCliRuntimeExecutionProvider(params: { }): string | undefined { const provider = normalizeProviderId(params.provider); const { runtime, matchedProvider } = resolveConfiguredRuntime({ ...params, provider }); - if (runtime === "pi") { + if (runtime === "openclaw") { return undefined; } if (!runtime || runtime === "auto") { diff --git a/src/agents/model-runtime-policy.test.ts b/src/agents/model-runtime-policy.test.ts index 0c63815b812..b46448dce32 100644 --- a/src/agents/model-runtime-policy.test.ts +++ b/src/agents/model-runtime-policy.test.ts @@ -55,7 +55,7 @@ afterEach(() => { describe("resolveModelRuntimePolicy", () => { it("ignores the QA force-runtime override when the private QA gate is unset", () => { delete process.env.OPENCLAW_BUILD_PRIVATE_QA; - process.env.OPENCLAW_QA_FORCE_RUNTIME = "pi"; + process.env.OPENCLAW_QA_FORCE_RUNTIME = "openclaw"; expect( resolveModelRuntimePolicy({ @@ -71,7 +71,7 @@ describe("resolveModelRuntimePolicy", () => { it("respects the QA force-runtime override when the private QA gate is set", () => { process.env.OPENCLAW_BUILD_PRIVATE_QA = "1"; - process.env.OPENCLAW_QA_FORCE_RUNTIME = "pi"; + process.env.OPENCLAW_QA_FORCE_RUNTIME = "openclaw"; expect( resolveModelRuntimePolicy({ @@ -80,7 +80,7 @@ describe("resolveModelRuntimePolicy", () => { modelId: "gpt-5.5", }), ).toEqual({ - policy: { id: "pi" }, + policy: { id: "openclaw" }, source: "model", }); }); @@ -106,7 +106,7 @@ describe("resolveModelRuntimePolicy", () => { agents: { defaults: { models: { - "vllm/*": { agentRuntime: { id: "pi" } }, + "vllm/*": { agentRuntime: { id: "openclaw" } }, }, }, }, @@ -119,7 +119,7 @@ describe("resolveModelRuntimePolicy", () => { modelId: "qwen-local", }), ).toEqual({ - policy: { id: "pi" }, + policy: { id: "openclaw" }, source: "model", matchedProvider: "vllm", }); @@ -130,7 +130,7 @@ describe("resolveModelRuntimePolicy", () => { agents: { defaults: { models: { - "vllm/*": { agentRuntime: { id: "pi" } }, + "vllm/*": { agentRuntime: { id: "openclaw" } }, }, }, }, @@ -142,7 +142,7 @@ describe("resolveModelRuntimePolicy", () => { provider: "vllm", }), ).toEqual({ - policy: { id: "pi" }, + policy: { id: "openclaw" }, source: "model", matchedProvider: "vllm", }); @@ -153,7 +153,7 @@ describe("resolveModelRuntimePolicy", () => { agents: { defaults: { models: { - "vllm/*": { agentRuntime: { id: "pi" } }, + "vllm/*": { agentRuntime: { id: "openclaw" } }, "vllm/qwen-local": { agentRuntime: { id: "codex" } }, }, }, @@ -178,7 +178,7 @@ describe("resolveModelRuntimePolicy", () => { agents: { defaults: { models: { - "vllm/*": { agentRuntime: { id: "pi" } }, + "vllm/*": { agentRuntime: { id: "openclaw" } }, }, }, }, @@ -209,7 +209,7 @@ describe("resolveModelRuntimePolicy", () => { agents: { defaults: { models: { - "vllm/*": { agentRuntime: { id: "pi" } }, + "vllm/*": { agentRuntime: { id: "openclaw" } }, }, }, }, @@ -231,7 +231,7 @@ describe("resolveModelRuntimePolicy", () => { modelId: "qwen-local", }), ).toEqual({ - policy: { id: "pi" }, + policy: { id: "openclaw" }, source: "model", matchedProvider: "vllm", }); diff --git a/src/agents/model-runtime-policy.ts b/src/agents/model-runtime-policy.ts index 866c8d167d8..4a217300cd9 100644 --- a/src/agents/model-runtime-policy.ts +++ b/src/agents/model-runtime-policy.ts @@ -228,7 +228,7 @@ export function resolveModelRuntimePolicy(params: { }): ResolvedModelRuntimePolicy { if (process.env.OPENCLAW_BUILD_PRIVATE_QA === "1") { const forcedRuntime = process.env.OPENCLAW_QA_FORCE_RUNTIME?.trim().toLowerCase(); - if (forcedRuntime === "pi" || forcedRuntime === "codex") { + if (forcedRuntime === "openclaw" || forcedRuntime === "codex") { return { policy: { id: forcedRuntime }, source: "model" }; } } diff --git a/src/agents/models-config.e2e-harness.ts b/src/agents/models-config.e2e-harness.ts index 52acd298cc2..6b6d9c33cd8 100644 --- a/src/agents/models-config.e2e-harness.ts +++ b/src/agents/models-config.e2e-harness.ts @@ -20,16 +20,13 @@ export function installModelsConfigTestHooks(opts?: { }) { let previousHome: string | undefined; let previousOpenClawAgentDir: string | undefined; - let previousPiCodingAgentDir: string | undefined; const originalFetch = globalThis.fetch; const shouldResetPluginLoaderState = opts?.resetPluginLoaderState !== false; beforeEach(() => { previousHome = process.env.HOME; previousOpenClawAgentDir = process.env.OPENCLAW_AGENT_DIR; - previousPiCodingAgentDir = process.env.PI_CODING_AGENT_DIR; delete process.env.OPENCLAW_AGENT_DIR; - delete process.env.PI_CODING_AGENT_DIR; clearRuntimeConfigSnapshot(); clearConfigCache(); if (shouldResetPluginLoaderState) { @@ -45,11 +42,6 @@ export function installModelsConfigTestHooks(opts?: { } else { process.env.OPENCLAW_AGENT_DIR = previousOpenClawAgentDir; } - if (previousPiCodingAgentDir === undefined) { - delete process.env.PI_CODING_AGENT_DIR; - } else { - process.env.PI_CODING_AGENT_DIR = previousPiCodingAgentDir; - } clearRuntimeConfigSnapshot(); clearConfigCache(); if (shouldResetPluginLoaderState) { @@ -108,7 +100,7 @@ export const MODELS_CONFIG_IMPLICIT_ENV_VARS = [ "OPENCLAW_AGENT_DIR", "OPENAI_API_KEY", "OPENROUTER_API_KEY", - "PI_CODING_AGENT_DIR", + "OPENCLAW_AGENT_DIR", "QIANFAN_API_KEY", "QWEN_API_KEY", "MODELSTUDIO_API_KEY", diff --git a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts index 4c6058c9f20..5b6d956fc78 100644 --- a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts +++ b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts @@ -153,7 +153,6 @@ describe("models-config", () => { const agentDir = path.join(home, "agent-empty"); // ensureAuthProfileStore merges the main auth store into non-main dirs; point main at our temp dir. process.env.OPENCLAW_AGENT_DIR = agentDir; - process.env.PI_CODING_AGENT_DIR = agentDir; const result = await ensureOpenClawModelsJson( { diff --git a/src/agents/models.profiles.live.test.ts b/src/agents/models.profiles.live.test.ts index dc508cbecfc..99ed44e4cbb 100644 --- a/src/agents/models.profiles.live.test.ts +++ b/src/agents/models.profiles.live.test.ts @@ -6,14 +6,20 @@ import { getProviders, type KnownProvider, type Model, -} from "@earendil-works/pi-ai"; +} from "openclaw/plugin-sdk/llm"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getRuntimeConfig } from "../config/config.js"; import { parseLiveCsvFilter } from "../media-generation/live-test-helpers.js"; import { runTasksWithConcurrency } from "../utils/run-with-concurrency.js"; +import { + discoverAuthStorage, + discoverModels, + normalizeDiscoveredAgentModel, +} from "./agent-model-discovery.js"; import { resolveDefaultAgentDir } from "./agent-scope.js"; import { externalCliDiscoveryForProviders } from "./auth-profiles/external-cli-discovery.js"; +import { isRateLimitErrorMessage } from "./embedded-agent-helpers/errors.js"; import { collectAnthropicApiKeys } from "./live-auth-keys.js"; import { isModelNotFoundErrorMessage } from "./live-model-errors.js"; import { @@ -55,12 +61,6 @@ import { import { getApiKeyForModel, requireApiKey } from "./model-auth.js"; import { shouldSuppressBuiltInModel } from "./model-suppression.js"; import { ensureOpenClawModelsJson } from "./models-config.js"; -import { isRateLimitErrorMessage } from "./pi-embedded-helpers/errors.js"; -import { - discoverAuthStorage, - discoverModels, - normalizeDiscoveredPiModel, -} from "./pi-model-discovery.js"; const LIVE = isLiveTestEnabled(); const DIRECT_ENABLED = Boolean(process.env.OPENCLAW_LIVE_MODELS?.trim()); @@ -107,7 +107,7 @@ function resolveKnownProvider(provider: string): KnownProvider | undefined { return getProviders().find((knownProvider) => knownProvider === normalized); } -function loadPrioritizedHighSignalModels(): Model[] { +function loadPrioritizedHighSignalModels(): Model[] { const idsByProvider = new Map>(); for (const ref of listPrioritizedHighSignalLiveModelRefs()) { const bucket = idsByProvider.get(ref.provider); @@ -118,7 +118,7 @@ function loadPrioritizedHighSignalModels(): Model[] { } } - const models: Model[] = []; + const models: Model[] = []; const seen = new Set(); for (const [provider, ids] of idsByProvider) { const knownProvider = resolveKnownProvider(provider); @@ -387,7 +387,7 @@ describe("resolveLiveModelsJsonTimeoutMs", () => { }); function resolveTestReasoning( - model: Model, + model: Model, ): "minimal" | "low" | "medium" | "high" | "xhigh" | undefined { if (!model.reasoning) { return undefined; @@ -411,7 +411,7 @@ function resolveTestReasoning( return "low"; } -function resolveLiveSystemPrompt(model: Model): string | undefined { +function resolveLiveSystemPrompt(model: Model): string | undefined { if (model.provider === "openai-codex") { return "You are a concise assistant. Follow the user's instruction exactly."; } @@ -423,7 +423,7 @@ describe("resolveLiveSystemPrompt", () => { expect( resolveLiveSystemPrompt({ provider: "openai-codex", - } as Model), + } as Model), ).toContain("Follow the user's instruction exactly."); }); @@ -431,7 +431,7 @@ describe("resolveLiveSystemPrompt", () => { expect( resolveLiveSystemPrompt({ provider: "openai", - } as Model), + } as Model), ).toBeUndefined(); }); @@ -513,7 +513,7 @@ describe("requireToolChoicePayload", () => { }); async function completeOkWithRetry(params: { - model: Model; + model: Model; apiKey: string; timeoutMs: number; progressLabel: string; @@ -555,7 +555,7 @@ async function completeOkWithRetry(params: { return await runOnce(256); } -function isDeepSeekV4Model(model: Pick, "id" | "provider">): boolean { +function isDeepSeekV4Model(model: Pick): boolean { return ( model.provider === "deepseek" && (model.id === "deepseek-v4-flash" || model.id === "deepseek-v4-pro") @@ -563,7 +563,7 @@ function isDeepSeekV4Model(model: Pick, "id" | "provider">): boolean } async function runDeepSeekV4ReplayRegression(params: { - model: Model; + model: Model; apiKey: string; timeoutMs: number; progressLabel: string; @@ -652,7 +652,7 @@ async function runDeepSeekV4ReplayRegression(params: { } async function runExtraTurnProbes(params: { - model: Model; + model: Model; apiKey: string; timeoutMs: number; progressLabel: string; @@ -825,7 +825,7 @@ describeLive("live models (profile keys)", () => { const failures: Array<{ model: string; error: string }> = []; const skipped: Array<{ model: string; reason: string }> = []; const candidates: Array<{ - model: Model; + model: Model; apiKeyInfo: Awaited>; }> = []; @@ -882,7 +882,7 @@ describeLive("live models (profile keys)", () => { continue; } candidates.push({ - model: normalizeDiscoveredPiModel(model, agentDir), + model: normalizeDiscoveredAgentModel(model, agentDir), apiKeyInfo, }); } catch (err) { diff --git a/src/agents/modes/interactive/components/diff.ts b/src/agents/modes/interactive/components/diff.ts new file mode 100644 index 00000000000..c1e5e8e5cbf --- /dev/null +++ b/src/agents/modes/interactive/components/diff.ts @@ -0,0 +1,158 @@ +import * as Diff from "diff"; +import { theme } from "../theme/theme.js"; + +/** + * Parse diff line to extract prefix, line number, and content. + * Format: "+123 content" or "-123 content" or " 123 content" or " ..." + */ +function parseDiffLine(line: string): { prefix: string; lineNum: string; content: string } | null { + const match = line.match(/^([+-\s])(\s*\d*)\s(.*)$/); + if (!match) { + return null; + } + return { prefix: match[1], lineNum: match[2], content: match[3] }; +} + +/** + * Replace tabs with spaces for consistent rendering. + */ +function replaceTabs(text: string): string { + return text.replace(/\t/g, " "); +} + +/** + * Compute word-level diff and render with inverse on changed parts. + * Uses diffWords which groups whitespace with adjacent words for cleaner highlighting. + * Strips leading whitespace from inverse to avoid highlighting indentation. + */ +function renderIntraLineDiff( + oldContent: string, + newContent: string, +): { removedLine: string; addedLine: string } { + const wordDiff = Diff.diffWords(oldContent, newContent); + + let removedLine = ""; + let addedLine = ""; + let isFirstRemoved = true; + let isFirstAdded = true; + + for (const part of wordDiff) { + if (part.removed) { + let value = part.value; + // Strip leading whitespace from the first removed part + if (isFirstRemoved) { + const leadingWs = value.match(/^(\s*)/)?.[1] || ""; + value = value.slice(leadingWs.length); + removedLine += leadingWs; + isFirstRemoved = false; + } + if (value) { + removedLine += theme.inverse(value); + } + } else if (part.added) { + let value = part.value; + // Strip leading whitespace from the first added part + if (isFirstAdded) { + const leadingWs = value.match(/^(\s*)/)?.[1] || ""; + value = value.slice(leadingWs.length); + addedLine += leadingWs; + isFirstAdded = false; + } + if (value) { + addedLine += theme.inverse(value); + } + } else { + removedLine += part.value; + addedLine += part.value; + } + } + + return { removedLine, addedLine }; +} + +export interface RenderDiffOptions { + /** File path (unused, kept for API compatibility) */ + filePath?: string; +} + +/** + * Render a diff string with colored lines and intra-line change highlighting. + * - Context lines: dim/gray + * - Removed lines: red, with inverse on changed tokens + * - Added lines: green, with inverse on changed tokens + */ +export function renderDiff(diffText: string, _options: RenderDiffOptions = {}): string { + const lines = diffText.split("\n"); + const result: string[] = []; + + let i = 0; + while (i < lines.length) { + const line = lines[i]; + const parsed = parseDiffLine(line); + + if (!parsed) { + result.push(theme.fg("toolDiffContext", line)); + i++; + continue; + } + + if (parsed.prefix === "-") { + // Collect consecutive removed lines + const removedLines: { lineNum: string; content: string }[] = []; + while (i < lines.length) { + const p = parseDiffLine(lines[i]); + if (!p || p.prefix !== "-") { + break; + } + removedLines.push({ lineNum: p.lineNum, content: p.content }); + i++; + } + + // Collect consecutive added lines + const addedLines: { lineNum: string; content: string }[] = []; + while (i < lines.length) { + const p = parseDiffLine(lines[i]); + if (!p || p.prefix !== "+") { + break; + } + addedLines.push({ lineNum: p.lineNum, content: p.content }); + i++; + } + + // Only do intra-line diffing when there's exactly one removed and one added line + // (indicating a single line modification). Otherwise, show lines as-is. + if (removedLines.length === 1 && addedLines.length === 1) { + const removed = removedLines[0]; + const added = addedLines[0]; + + const { removedLine, addedLine } = renderIntraLineDiff( + replaceTabs(removed.content), + replaceTabs(added.content), + ); + + result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${removedLine}`)); + result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${addedLine}`)); + } else { + // Show all removed lines first, then all added lines + for (const removed of removedLines) { + result.push( + theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`), + ); + } + for (const added of addedLines) { + result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`)); + } + } + } else if (parsed.prefix === "+") { + // Standalone added line + result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`)); + i++; + } else { + // Context line + result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`)); + i++; + } + } + + return result.join("\n"); +} diff --git a/src/agents/modes/interactive/components/keybinding-hints.ts b/src/agents/modes/interactive/components/keybinding-hints.ts new file mode 100644 index 00000000000..eef1cc51cd1 --- /dev/null +++ b/src/agents/modes/interactive/components/keybinding-hints.ts @@ -0,0 +1,53 @@ +/** + * Utilities for formatting keybinding hints in the UI. + */ + +import { getKeybindings, type Keybinding, type KeyId } from "@earendil-works/pi-tui"; +import { theme } from "../theme/theme.js"; + +export interface KeyTextFormatOptions { + capitalize?: boolean; +} + +function formatKeyPart(part: string, options: KeyTextFormatOptions): string { + const displayPart = + process.platform === "darwin" && part.toLowerCase() === "alt" ? "option" : part; + return options.capitalize + ? displayPart.charAt(0).toUpperCase() + displayPart.slice(1) + : displayPart; +} + +export function formatKeyText(key: string, options: KeyTextFormatOptions = {}): string { + return key + .split("/") + .map((k) => + k + .split("+") + .map((part) => formatKeyPart(part, options)) + .join("+"), + ) + .join("/"); +} + +function formatKeys(keys: KeyId[], options: KeyTextFormatOptions = {}): string { + if (keys.length === 0) { + return ""; + } + return formatKeyText(keys.join("/"), options); +} + +export function keyText(keybinding: Keybinding): string { + return formatKeys(getKeybindings().getKeys(keybinding)); +} + +export function keyDisplayText(keybinding: Keybinding): string { + return formatKeys(getKeybindings().getKeys(keybinding), { capitalize: true }); +} + +export function keyHint(keybinding: Keybinding, description: string): string { + return theme.fg("dim", keyText(keybinding)) + theme.fg("muted", ` ${description}`); +} + +export function rawKeyHint(key: string, description: string): string { + return theme.fg("dim", formatKeyText(key)) + theme.fg("muted", ` ${description}`); +} diff --git a/src/agents/modes/interactive/components/visual-truncate.ts b/src/agents/modes/interactive/components/visual-truncate.ts new file mode 100644 index 00000000000..43a359a0cad --- /dev/null +++ b/src/agents/modes/interactive/components/visual-truncate.ts @@ -0,0 +1,50 @@ +/** + * Shared utility for truncating text to visual lines (accounting for line wrapping). + * Used by both tool-execution.ts and bash-execution.ts for consistent behavior. + */ + +import { Text } from "@earendil-works/pi-tui"; + +export interface VisualTruncateResult { + /** The visual lines to display */ + visualLines: string[]; + /** Number of visual lines that were skipped (hidden) */ + skippedCount: number; +} + +/** + * Truncate text to a maximum number of visual lines (from the end). + * This accounts for line wrapping based on terminal width. + * + * @param text - The text content (may contain newlines) + * @param maxVisualLines - Maximum number of visual lines to show + * @param width - Terminal/render width + * @param paddingX - Horizontal padding for Text component (default 0). + * Use 0 when result will be placed in a Box (Box adds its own padding). + * Use 1 when result will be placed in a plain Container. + * @returns The truncated visual lines and count of skipped lines + */ +export function truncateToVisualLines( + text: string, + maxVisualLines: number, + width: number, + paddingX: number = 0, +): VisualTruncateResult { + if (!text) { + return { visualLines: [], skippedCount: 0 }; + } + + // Create a temporary Text component to render and get visual lines + const tempText = new Text(text, paddingX, 0); + const allVisualLines = tempText.render(width); + + if (allVisualLines.length <= maxVisualLines) { + return { visualLines: allVisualLines, skippedCount: 0 }; + } + + // Take the last N visual lines + const truncatedLines = allVisualLines.slice(-maxVisualLines); + const skippedCount = allVisualLines.length - maxVisualLines; + + return { visualLines: truncatedLines, skippedCount }; +} diff --git a/src/agents/modes/interactive/theme/dark.json b/src/agents/modes/interactive/theme/dark.json new file mode 100644 index 00000000000..3d149113a3b --- /dev/null +++ b/src/agents/modes/interactive/theme/dark.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://raw.githubusercontent.com/openclaw/openclaw/main/src/agents/modes/interactive/theme/theme-schema.json", + "name": "dark", + "vars": { + "cyan": "#00d7ff", + "blue": "#5f87ff", + "green": "#b5bd68", + "red": "#cc6666", + "yellow": "#ffff00", + "text": "#d4d4d4", + "gray": "#808080", + "dimGray": "#666666", + "darkGray": "#505050", + "accent": "#8abeb7", + "selectedBg": "#3a3a4a", + "userMsgBg": "#343541", + "toolPendingBg": "#282832", + "toolSuccessBg": "#283228", + "toolErrorBg": "#3c2828", + "customMsgBg": "#2d2838" + }, + "colors": { + "accent": "accent", + "border": "blue", + "borderAccent": "cyan", + "borderMuted": "darkGray", + "success": "green", + "error": "red", + "warning": "yellow", + "muted": "gray", + "dim": "dimGray", + "text": "text", + "thinkingText": "gray", + + "selectedBg": "selectedBg", + "userMessageBg": "userMsgBg", + "userMessageText": "text", + "customMessageBg": "customMsgBg", + "customMessageText": "text", + "customMessageLabel": "#9575cd", + "toolPendingBg": "toolPendingBg", + "toolSuccessBg": "toolSuccessBg", + "toolErrorBg": "toolErrorBg", + "toolTitle": "text", + "toolOutput": "gray", + + "mdHeading": "#f0c674", + "mdLink": "#81a2be", + "mdLinkUrl": "dimGray", + "mdCode": "accent", + "mdCodeBlock": "green", + "mdCodeBlockBorder": "gray", + "mdQuote": "gray", + "mdQuoteBorder": "gray", + "mdHr": "gray", + "mdListBullet": "accent", + + "toolDiffAdded": "green", + "toolDiffRemoved": "red", + "toolDiffContext": "gray", + + "syntaxComment": "#6A9955", + "syntaxKeyword": "#569CD6", + "syntaxFunction": "#DCDCAA", + "syntaxVariable": "#9CDCFE", + "syntaxString": "#CE9178", + "syntaxNumber": "#B5CEA8", + "syntaxType": "#4EC9B0", + "syntaxOperator": "#D4D4D4", + "syntaxPunctuation": "#D4D4D4", + + "thinkingOff": "darkGray", + "thinkingMinimal": "#6e6e6e", + "thinkingLow": "#5f87af", + "thinkingMedium": "#81a2be", + "thinkingHigh": "#b294bb", + "thinkingXhigh": "#d183e8", + + "bashMode": "green" + }, + "export": { + "pageBg": "#18181e", + "cardBg": "#1e1e24", + "infoBg": "#3c3728" + } +} diff --git a/src/agents/modes/interactive/theme/light.json b/src/agents/modes/interactive/theme/light.json new file mode 100644 index 00000000000..f1c537f394d --- /dev/null +++ b/src/agents/modes/interactive/theme/light.json @@ -0,0 +1,85 @@ +{ + "$schema": "https://raw.githubusercontent.com/openclaw/openclaw/main/src/agents/modes/interactive/theme/theme-schema.json", + "name": "light", + "vars": { + "teal": "#5a8080", + "blue": "#547da7", + "green": "#588458", + "red": "#aa5555", + "yellow": "#9a7326", + "text": "#1f2328", + "mediumGray": "#6c6c6c", + "dimGray": "#767676", + "lightGray": "#b0b0b0", + "selectedBg": "#d0d0e0", + "userMsgBg": "#e8e8e8", + "toolPendingBg": "#e8e8f0", + "toolSuccessBg": "#e8f0e8", + "toolErrorBg": "#f0e8e8", + "customMsgBg": "#ede7f6" + }, + "colors": { + "accent": "teal", + "border": "blue", + "borderAccent": "teal", + "borderMuted": "lightGray", + "success": "green", + "error": "red", + "warning": "yellow", + "muted": "mediumGray", + "dim": "dimGray", + "text": "text", + "thinkingText": "mediumGray", + + "selectedBg": "selectedBg", + "userMessageBg": "userMsgBg", + "userMessageText": "text", + "customMessageBg": "customMsgBg", + "customMessageText": "text", + "customMessageLabel": "#7e57c2", + "toolPendingBg": "toolPendingBg", + "toolSuccessBg": "toolSuccessBg", + "toolErrorBg": "toolErrorBg", + "toolTitle": "text", + "toolOutput": "mediumGray", + + "mdHeading": "yellow", + "mdLink": "blue", + "mdLinkUrl": "dimGray", + "mdCode": "teal", + "mdCodeBlock": "green", + "mdCodeBlockBorder": "mediumGray", + "mdQuote": "mediumGray", + "mdQuoteBorder": "mediumGray", + "mdHr": "mediumGray", + "mdListBullet": "green", + + "toolDiffAdded": "green", + "toolDiffRemoved": "red", + "toolDiffContext": "mediumGray", + + "syntaxComment": "#008000", + "syntaxKeyword": "#0000FF", + "syntaxFunction": "#795E26", + "syntaxVariable": "#001080", + "syntaxString": "#A31515", + "syntaxNumber": "#098658", + "syntaxType": "#267F99", + "syntaxOperator": "#000000", + "syntaxPunctuation": "#000000", + + "thinkingOff": "lightGray", + "thinkingMinimal": "#767676", + "thinkingLow": "blue", + "thinkingMedium": "teal", + "thinkingHigh": "#875f87", + "thinkingXhigh": "#8b008b", + + "bashMode": "green" + }, + "export": { + "pageBg": "#f8f8f8", + "cardBg": "#ffffff", + "infoBg": "#fffae6" + } +} diff --git a/src/agents/modes/interactive/theme/theme-schema.json b/src/agents/modes/interactive/theme/theme-schema.json new file mode 100644 index 00000000000..91bc51be7a4 --- /dev/null +++ b/src/agents/modes/interactive/theme/theme-schema.json @@ -0,0 +1,335 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "OpenClaw Agent Theme", + "description": "Theme schema for OpenClaw agent", + "type": "object", + "required": ["name", "colors"], + "properties": { + "$schema": { + "type": "string", + "description": "JSON schema reference" + }, + "name": { + "type": "string", + "description": "Theme name" + }, + "vars": { + "type": "object", + "description": "Reusable color variables", + "additionalProperties": { + "oneOf": [ + { + "type": "string", + "description": "Hex color (#RRGGBB), variable reference, or empty string for terminal default" + }, + { + "type": "integer", + "minimum": 0, + "maximum": 255, + "description": "256-color palette index (0-255)" + } + ] + } + }, + "colors": { + "type": "object", + "description": "Theme color definitions (all required)", + "required": [ + "accent", + "border", + "borderAccent", + "borderMuted", + "success", + "error", + "warning", + "muted", + "dim", + "text", + "thinkingText", + "selectedBg", + "userMessageBg", + "userMessageText", + "customMessageBg", + "customMessageText", + "customMessageLabel", + "toolPendingBg", + "toolSuccessBg", + "toolErrorBg", + "toolTitle", + "toolOutput", + "mdHeading", + "mdLink", + "mdLinkUrl", + "mdCode", + "mdCodeBlock", + "mdCodeBlockBorder", + "mdQuote", + "mdQuoteBorder", + "mdHr", + "mdListBullet", + "toolDiffAdded", + "toolDiffRemoved", + "toolDiffContext", + "syntaxComment", + "syntaxKeyword", + "syntaxFunction", + "syntaxVariable", + "syntaxString", + "syntaxNumber", + "syntaxType", + "syntaxOperator", + "syntaxPunctuation", + "thinkingOff", + "thinkingMinimal", + "thinkingLow", + "thinkingMedium", + "thinkingHigh", + "thinkingXhigh", + "bashMode" + ], + "properties": { + "accent": { + "$ref": "#/$defs/colorValue", + "description": "Primary accent color (logo, selected items, cursor)" + }, + "border": { + "$ref": "#/$defs/colorValue", + "description": "Normal borders" + }, + "borderAccent": { + "$ref": "#/$defs/colorValue", + "description": "Highlighted borders" + }, + "borderMuted": { + "$ref": "#/$defs/colorValue", + "description": "Subtle borders" + }, + "success": { + "$ref": "#/$defs/colorValue", + "description": "Success states" + }, + "error": { + "$ref": "#/$defs/colorValue", + "description": "Error states" + }, + "warning": { + "$ref": "#/$defs/colorValue", + "description": "Warning states" + }, + "muted": { + "$ref": "#/$defs/colorValue", + "description": "Secondary/dimmed text" + }, + "dim": { + "$ref": "#/$defs/colorValue", + "description": "Very dimmed text (more subtle than muted)" + }, + "text": { + "$ref": "#/$defs/colorValue", + "description": "Default text color (usually empty string)" + }, + "thinkingText": { + "$ref": "#/$defs/colorValue", + "description": "Thinking block text color" + }, + "selectedBg": { + "$ref": "#/$defs/colorValue", + "description": "Selected item background" + }, + "userMessageBg": { + "$ref": "#/$defs/colorValue", + "description": "User message background" + }, + "userMessageText": { + "$ref": "#/$defs/colorValue", + "description": "User message text color" + }, + "customMessageBg": { + "$ref": "#/$defs/colorValue", + "description": "Custom message background (hook-injected messages)" + }, + "customMessageText": { + "$ref": "#/$defs/colorValue", + "description": "Custom message text color" + }, + "customMessageLabel": { + "$ref": "#/$defs/colorValue", + "description": "Custom message type label color" + }, + "toolPendingBg": { + "$ref": "#/$defs/colorValue", + "description": "Tool execution box (pending state)" + }, + "toolSuccessBg": { + "$ref": "#/$defs/colorValue", + "description": "Tool execution box (success state)" + }, + "toolErrorBg": { + "$ref": "#/$defs/colorValue", + "description": "Tool execution box (error state)" + }, + "toolTitle": { + "$ref": "#/$defs/colorValue", + "description": "Tool execution box title color" + }, + "toolOutput": { + "$ref": "#/$defs/colorValue", + "description": "Tool execution box output text color" + }, + "mdHeading": { + "$ref": "#/$defs/colorValue", + "description": "Markdown heading text" + }, + "mdLink": { + "$ref": "#/$defs/colorValue", + "description": "Markdown link text" + }, + "mdLinkUrl": { + "$ref": "#/$defs/colorValue", + "description": "Markdown link URL" + }, + "mdCode": { + "$ref": "#/$defs/colorValue", + "description": "Markdown inline code" + }, + "mdCodeBlock": { + "$ref": "#/$defs/colorValue", + "description": "Markdown code block content" + }, + "mdCodeBlockBorder": { + "$ref": "#/$defs/colorValue", + "description": "Markdown code block fences" + }, + "mdQuote": { + "$ref": "#/$defs/colorValue", + "description": "Markdown blockquote text" + }, + "mdQuoteBorder": { + "$ref": "#/$defs/colorValue", + "description": "Markdown blockquote border" + }, + "mdHr": { + "$ref": "#/$defs/colorValue", + "description": "Markdown horizontal rule" + }, + "mdListBullet": { + "$ref": "#/$defs/colorValue", + "description": "Markdown list bullets/numbers" + }, + "toolDiffAdded": { + "$ref": "#/$defs/colorValue", + "description": "Added lines in tool diffs" + }, + "toolDiffRemoved": { + "$ref": "#/$defs/colorValue", + "description": "Removed lines in tool diffs" + }, + "toolDiffContext": { + "$ref": "#/$defs/colorValue", + "description": "Context lines in tool diffs" + }, + "syntaxComment": { + "$ref": "#/$defs/colorValue", + "description": "Syntax highlighting: comments" + }, + "syntaxKeyword": { + "$ref": "#/$defs/colorValue", + "description": "Syntax highlighting: keywords" + }, + "syntaxFunction": { + "$ref": "#/$defs/colorValue", + "description": "Syntax highlighting: function names" + }, + "syntaxVariable": { + "$ref": "#/$defs/colorValue", + "description": "Syntax highlighting: variable names" + }, + "syntaxString": { + "$ref": "#/$defs/colorValue", + "description": "Syntax highlighting: string literals" + }, + "syntaxNumber": { + "$ref": "#/$defs/colorValue", + "description": "Syntax highlighting: number literals" + }, + "syntaxType": { + "$ref": "#/$defs/colorValue", + "description": "Syntax highlighting: type names" + }, + "syntaxOperator": { + "$ref": "#/$defs/colorValue", + "description": "Syntax highlighting: operators" + }, + "syntaxPunctuation": { + "$ref": "#/$defs/colorValue", + "description": "Syntax highlighting: punctuation" + }, + "thinkingOff": { + "$ref": "#/$defs/colorValue", + "description": "Thinking level border: off" + }, + "thinkingMinimal": { + "$ref": "#/$defs/colorValue", + "description": "Thinking level border: minimal" + }, + "thinkingLow": { + "$ref": "#/$defs/colorValue", + "description": "Thinking level border: low" + }, + "thinkingMedium": { + "$ref": "#/$defs/colorValue", + "description": "Thinking level border: medium" + }, + "thinkingHigh": { + "$ref": "#/$defs/colorValue", + "description": "Thinking level border: high" + }, + "thinkingXhigh": { + "$ref": "#/$defs/colorValue", + "description": "Thinking level border: xhigh (OpenAI codex-max only)" + }, + "bashMode": { + "$ref": "#/$defs/colorValue", + "description": "Editor border color in bash mode" + } + }, + "additionalProperties": false + }, + "export": { + "type": "object", + "description": "Optional colors for HTML export (defaults derived from userMessageBg if not specified)", + "properties": { + "pageBg": { + "$ref": "#/$defs/colorValue", + "description": "Page background color" + }, + "cardBg": { + "$ref": "#/$defs/colorValue", + "description": "Card/container background color" + }, + "infoBg": { + "$ref": "#/$defs/colorValue", + "description": "Info sections background (system prompt, notices)" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "$defs": { + "colorValue": { + "oneOf": [ + { + "type": "string", + "description": "Hex color (#RRGGBB), variable reference, or empty string for terminal default" + }, + { + "type": "integer", + "minimum": 0, + "maximum": 255, + "description": "256-color palette index (0-255)" + } + ] + } + } +} diff --git a/src/agents/modes/interactive/theme/theme.ts b/src/agents/modes/interactive/theme/theme.ts new file mode 100644 index 00000000000..ff4adb3402b --- /dev/null +++ b/src/agents/modes/interactive/theme/theme.ts @@ -0,0 +1,1272 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; +import { + type EditorTheme, + getCapabilities, + type MarkdownTheme, + type SelectListTheme, + type SettingsListTheme, +} from "@earendil-works/pi-tui"; +import chalk from "chalk"; +import { type Static, Type } from "typebox"; +import { Compile } from "typebox/compile"; +import { getCustomThemesDir, getThemesDir } from "../../../config.js"; +import type { SourceInfo } from "../../../sessions/source-info.js"; +import { closeWatcher, watchWithErrorHandler } from "../../../utils/fs-watch.js"; +import { highlight, supportsLanguage } from "../../../utils/syntax-highlight.js"; + +// ============================================================================ +// Types & Schema +// ============================================================================ + +const ColorValueSchema = Type.Union([ + Type.String(), // hex "#ff0000", var ref "primary", or empty "" + Type.Integer({ minimum: 0, maximum: 255 }), // 256-color index +]); + +type ColorValue = Static; + +const ThemeJsonSchema = Type.Object({ + $schema: Type.Optional(Type.String()), + name: Type.String(), + vars: Type.Optional(Type.Record(Type.String(), ColorValueSchema)), + colors: Type.Object({ + // Core UI (10 colors) + accent: ColorValueSchema, + border: ColorValueSchema, + borderAccent: ColorValueSchema, + borderMuted: ColorValueSchema, + success: ColorValueSchema, + error: ColorValueSchema, + warning: ColorValueSchema, + muted: ColorValueSchema, + dim: ColorValueSchema, + text: ColorValueSchema, + thinkingText: ColorValueSchema, + // Backgrounds & Content Text (11 colors) + selectedBg: ColorValueSchema, + userMessageBg: ColorValueSchema, + userMessageText: ColorValueSchema, + customMessageBg: ColorValueSchema, + customMessageText: ColorValueSchema, + customMessageLabel: ColorValueSchema, + toolPendingBg: ColorValueSchema, + toolSuccessBg: ColorValueSchema, + toolErrorBg: ColorValueSchema, + toolTitle: ColorValueSchema, + toolOutput: ColorValueSchema, + // Markdown (10 colors) + mdHeading: ColorValueSchema, + mdLink: ColorValueSchema, + mdLinkUrl: ColorValueSchema, + mdCode: ColorValueSchema, + mdCodeBlock: ColorValueSchema, + mdCodeBlockBorder: ColorValueSchema, + mdQuote: ColorValueSchema, + mdQuoteBorder: ColorValueSchema, + mdHr: ColorValueSchema, + mdListBullet: ColorValueSchema, + // Tool Diffs (3 colors) + toolDiffAdded: ColorValueSchema, + toolDiffRemoved: ColorValueSchema, + toolDiffContext: ColorValueSchema, + // Syntax Highlighting (9 colors) + syntaxComment: ColorValueSchema, + syntaxKeyword: ColorValueSchema, + syntaxFunction: ColorValueSchema, + syntaxVariable: ColorValueSchema, + syntaxString: ColorValueSchema, + syntaxNumber: ColorValueSchema, + syntaxType: ColorValueSchema, + syntaxOperator: ColorValueSchema, + syntaxPunctuation: ColorValueSchema, + // Thinking Level Borders (6 colors) + thinkingOff: ColorValueSchema, + thinkingMinimal: ColorValueSchema, + thinkingLow: ColorValueSchema, + thinkingMedium: ColorValueSchema, + thinkingHigh: ColorValueSchema, + thinkingXhigh: ColorValueSchema, + // Bash Mode (1 color) + bashMode: ColorValueSchema, + }), + export: Type.Optional( + Type.Object({ + pageBg: Type.Optional(ColorValueSchema), + cardBg: Type.Optional(ColorValueSchema), + infoBg: Type.Optional(ColorValueSchema), + }), + ), +}); + +type ThemeJson = Static; + +const validateThemeJson = Compile(ThemeJsonSchema); + +export type ThemeColor = + | "accent" + | "border" + | "borderAccent" + | "borderMuted" + | "success" + | "error" + | "warning" + | "muted" + | "dim" + | "text" + | "thinkingText" + | "userMessageText" + | "customMessageText" + | "customMessageLabel" + | "toolTitle" + | "toolOutput" + | "mdHeading" + | "mdLink" + | "mdLinkUrl" + | "mdCode" + | "mdCodeBlock" + | "mdCodeBlockBorder" + | "mdQuote" + | "mdQuoteBorder" + | "mdHr" + | "mdListBullet" + | "toolDiffAdded" + | "toolDiffRemoved" + | "toolDiffContext" + | "syntaxComment" + | "syntaxKeyword" + | "syntaxFunction" + | "syntaxVariable" + | "syntaxString" + | "syntaxNumber" + | "syntaxType" + | "syntaxOperator" + | "syntaxPunctuation" + | "thinkingOff" + | "thinkingMinimal" + | "thinkingLow" + | "thinkingMedium" + | "thinkingHigh" + | "thinkingXhigh" + | "bashMode"; + +export type ThemeBg = + | "selectedBg" + | "userMessageBg" + | "customMessageBg" + | "toolPendingBg" + | "toolSuccessBg" + | "toolErrorBg"; + +type ColorMode = "truecolor" | "256color"; + +// ============================================================================ +// Color Utilities +// ============================================================================ + +function hexToRgb(hex: string): { r: number; g: number; b: number } { + const cleaned = hex.replace("#", ""); + if (cleaned.length !== 6) { + throw new Error(`Invalid hex color: ${hex}`); + } + const r = Number.parseInt(cleaned.slice(0, 2), 16); + const g = Number.parseInt(cleaned.slice(2, 4), 16); + const b = Number.parseInt(cleaned.slice(4, 6), 16); + if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) { + throw new Error(`Invalid hex color: ${hex}`); + } + return { r, g, b }; +} + +// The 6x6x6 color cube channel values (indices 0-5) +const CUBE_VALUES = [0, 95, 135, 175, 215, 255]; + +// Grayscale ramp values (indices 232-255, 24 grays from 8 to 238) +const GRAY_VALUES = Array.from({ length: 24 }, (_, i) => 8 + i * 10); + +function findClosestCubeIndex(value: number): number { + let minDist = Infinity; + let minIdx = 0; + for (let i = 0; i < CUBE_VALUES.length; i++) { + const dist = Math.abs(value - CUBE_VALUES[i]); + if (dist < minDist) { + minDist = dist; + minIdx = i; + } + } + return minIdx; +} + +function findClosestGrayIndex(gray: number): number { + let minDist = Infinity; + let minIdx = 0; + for (let i = 0; i < GRAY_VALUES.length; i++) { + const dist = Math.abs(gray - GRAY_VALUES[i]); + if (dist < minDist) { + minDist = dist; + minIdx = i; + } + } + return minIdx; +} + +function colorDistance( + r1: number, + g1: number, + b1: number, + r2: number, + g2: number, + b2: number, +): number { + // Weighted Euclidean distance (human eye is more sensitive to green) + const dr = r1 - r2; + const dg = g1 - g2; + const db = b1 - b2; + return dr * dr * 0.299 + dg * dg * 0.587 + db * db * 0.114; +} + +function rgbTo256(r: number, g: number, b: number): number { + // Find closest color in the 6x6x6 cube + const rIdx = findClosestCubeIndex(r); + const gIdx = findClosestCubeIndex(g); + const bIdx = findClosestCubeIndex(b); + const cubeR = CUBE_VALUES[rIdx]; + const cubeG = CUBE_VALUES[gIdx]; + const cubeB = CUBE_VALUES[bIdx]; + const cubeIndex = 16 + 36 * rIdx + 6 * gIdx + bIdx; + const cubeDist = colorDistance(r, g, b, cubeR, cubeG, cubeB); + + // Find closest grayscale + const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b); + const grayIdx = findClosestGrayIndex(gray); + const grayValue = GRAY_VALUES[grayIdx]; + const grayIndex = 232 + grayIdx; + const grayDist = colorDistance(r, g, b, grayValue, grayValue, grayValue); + + // Check if color has noticeable saturation (hue matters) + // If max-min spread is significant, prefer cube to preserve tint + const maxC = Math.max(r, g, b); + const minC = Math.min(r, g, b); + const spread = maxC - minC; + + // Only consider grayscale if color is nearly neutral (spread < 10) + // AND grayscale is actually closer + if (spread < 10 && grayDist < cubeDist) { + return grayIndex; + } + + return cubeIndex; +} + +function hexTo256(hex: string): number { + const { r, g, b } = hexToRgb(hex); + return rgbTo256(r, g, b); +} + +function fgAnsi(color: string | number, mode: ColorMode): string { + if (color === "") { + return "\x1b[39m"; + } + if (typeof color === "number") { + return `\x1b[38;5;${color}m`; + } + if (color.startsWith("#")) { + if (mode === "truecolor") { + const { r, g, b } = hexToRgb(color); + return `\x1b[38;2;${r};${g};${b}m`; + } + const index = hexTo256(color); + return `\x1b[38;5;${index}m`; + } + throw new Error(`Invalid color value: ${color}`); +} + +function bgAnsi(color: string | number, mode: ColorMode): string { + if (color === "") { + return "\x1b[49m"; + } + if (typeof color === "number") { + return `\x1b[48;5;${color}m`; + } + if (color.startsWith("#")) { + if (mode === "truecolor") { + const { r, g, b } = hexToRgb(color); + return `\x1b[48;2;${r};${g};${b}m`; + } + const index = hexTo256(color); + return `\x1b[48;5;${index}m`; + } + throw new Error(`Invalid color value: ${color}`); +} + +function resolveVarRefs( + value: ColorValue, + vars: Record, + visited = new Set(), +): string | number { + if (typeof value === "number" || value === "" || value.startsWith("#")) { + return value; + } + if (visited.has(value)) { + throw new Error(`Circular variable reference detected: ${value}`); + } + if (!(value in vars)) { + throw new Error(`Variable reference not found: ${value}`); + } + visited.add(value); + return resolveVarRefs(vars[value], vars, visited); +} + +function resolveThemeColors>( + colors: T, + vars: Record = {}, +): Record { + const resolved: Record = {}; + for (const [key, value] of Object.entries(colors)) { + resolved[key] = resolveVarRefs(value, vars); + } + return resolved as Record; +} + +// ============================================================================ +// Theme Class +// ============================================================================ + +export class Theme { + readonly name?: string; + readonly sourcePath?: string; + sourceInfo?: SourceInfo; + private fgColors: Map; + private bgColors: Map; + private mode: ColorMode; + + constructor( + fgColors: Record, + bgColors: Record, + mode: ColorMode, + options: { name?: string; sourcePath?: string; sourceInfo?: SourceInfo } = {}, + ) { + this.name = options.name; + this.sourcePath = options.sourcePath; + this.sourceInfo = options.sourceInfo; + this.mode = mode; + this.fgColors = new Map(); + for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) { + this.fgColors.set(key, fgAnsi(value, mode)); + } + this.bgColors = new Map(); + for (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) { + this.bgColors.set(key, bgAnsi(value, mode)); + } + } + + fg(color: ThemeColor, text: string): string { + const ansi = this.fgColors.get(color); + if (!ansi) { + throw new Error(`Unknown theme color: ${color}`); + } + return `${ansi}${text}\x1b[39m`; // Reset only foreground color + } + + bg(color: ThemeBg, text: string): string { + const ansi = this.bgColors.get(color); + if (!ansi) { + throw new Error(`Unknown theme background color: ${color}`); + } + return `${ansi}${text}\x1b[49m`; // Reset only background color + } + + bold(text: string): string { + return chalk.bold(text); + } + + italic(text: string): string { + return chalk.italic(text); + } + + underline(text: string): string { + return chalk.underline(text); + } + + inverse(text: string): string { + return chalk.inverse(text); + } + + strikethrough(text: string): string { + return chalk.strikethrough(text); + } + + getFgAnsi(color: ThemeColor): string { + const ansi = this.fgColors.get(color); + if (!ansi) { + throw new Error(`Unknown theme color: ${color}`); + } + return ansi; + } + + getBgAnsi(color: ThemeBg): string { + const ansi = this.bgColors.get(color); + if (!ansi) { + throw new Error(`Unknown theme background color: ${color}`); + } + return ansi; + } + + getColorMode(): ColorMode { + return this.mode; + } + + getThinkingBorderColor( + level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh", + ): (str: string) => string { + // Map thinking levels to dedicated theme colors + switch (level) { + case "off": + return (str: string) => this.fg("thinkingOff", str); + case "minimal": + return (str: string) => this.fg("thinkingMinimal", str); + case "low": + return (str: string) => this.fg("thinkingLow", str); + case "medium": + return (str: string) => this.fg("thinkingMedium", str); + case "high": + return (str: string) => this.fg("thinkingHigh", str); + case "xhigh": + return (str: string) => this.fg("thinkingXhigh", str); + default: + return (str: string) => this.fg("thinkingOff", str); + } + } + + getBashModeBorderColor(): (str: string) => string { + return (str: string) => this.fg("bashMode", str); + } +} + +// ============================================================================ +// Theme Loading +// ============================================================================ + +let BUILTIN_THEMES: Record | undefined; + +function getBuiltinThemes(): Record { + if (!BUILTIN_THEMES) { + const themesDir = getThemesDir(); + const darkPath = path.join(themesDir, "dark.json"); + const lightPath = path.join(themesDir, "light.json"); + BUILTIN_THEMES = { + dark: JSON.parse(fs.readFileSync(darkPath, "utf-8")) as ThemeJson, + light: JSON.parse(fs.readFileSync(lightPath, "utf-8")) as ThemeJson, + }; + } + return BUILTIN_THEMES; +} + +export function getAvailableThemes(): string[] { + const themes = new Set(Object.keys(getBuiltinThemes())); + const customThemesDir = getCustomThemesDir(); + if (fs.existsSync(customThemesDir)) { + const files = fs.readdirSync(customThemesDir); + for (const file of files) { + if (file.endsWith(".json")) { + themes.add(file.slice(0, -5)); + } + } + } + for (const name of registeredThemes.keys()) { + themes.add(name); + } + return Array.from(themes).toSorted(); +} + +export interface ThemeInfo { + name: string; + path: string | undefined; +} + +export function getAvailableThemesWithPaths(): ThemeInfo[] { + const themesDir = getThemesDir(); + const customThemesDir = getCustomThemesDir(); + const result: ThemeInfo[] = []; + + // Built-in themes + for (const name of Object.keys(getBuiltinThemes())) { + result.push({ name, path: path.join(themesDir, `${name}.json`) }); + } + + // Custom themes + if (fs.existsSync(customThemesDir)) { + for (const file of fs.readdirSync(customThemesDir)) { + if (file.endsWith(".json")) { + const name = file.slice(0, -5); + if (!result.some((t) => t.name === name)) { + result.push({ name, path: path.join(customThemesDir, file) }); + } + } + } + } + + for (const [name, theme] of registeredThemes.entries()) { + if (!result.some((t) => t.name === name)) { + result.push({ name, path: theme.sourcePath }); + } + } + + return result.toSorted((a, b) => a.name.localeCompare(b.name)); +} + +function parseThemeJson(label: string, json: unknown): ThemeJson { + if (!validateThemeJson.Check(json)) { + const errors = Array.from(validateThemeJson.Errors(json)); + const missingColors = new Set(); + const otherErrors: string[] = []; + + for (const error of errors) { + if (error.keyword === "required" && error.instancePath === "/colors") { + const requiredProperties = (error.params as { requiredProperties?: string[] }) + .requiredProperties; + for (const requiredProperty of requiredProperties ?? []) { + missingColors.add(requiredProperty); + } + continue; + } + + const path = error.instancePath || "/"; + otherErrors.push(` - ${path}: ${error.message}`); + } + + let errorMessage = `Invalid theme "${label}":\n`; + if (missingColors.size > 0) { + errorMessage += "\nMissing required color tokens:\n"; + errorMessage += Array.from(missingColors) + .toSorted() + .map((color) => ` - ${color}`) + .join("\n"); + errorMessage += '\n\nPlease add these colors to your theme\'s "colors" object.'; + errorMessage += "\nSee the built-in themes (dark.json, light.json) for reference values."; + } + if (otherErrors.length > 0) { + errorMessage += `\n\nOther errors:\n${otherErrors.join("\n")}`; + } + + throw new Error(errorMessage); + } + + return json; +} + +function parseThemeJsonContent(label: string, content: string): ThemeJson { + let json: unknown; + try { + json = JSON.parse(content); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`Failed to parse theme ${label}: ${message}`, { cause: error }); + } + return parseThemeJson(label, json); +} + +function loadThemeJson(name: string): ThemeJson { + const builtinThemes = getBuiltinThemes(); + if (name in builtinThemes) { + return builtinThemes[name]; + } + const registeredTheme = registeredThemes.get(name); + if (registeredTheme?.sourcePath) { + const content = fs.readFileSync(registeredTheme.sourcePath, "utf-8"); + return parseThemeJsonContent(registeredTheme.sourcePath, content); + } + if (registeredTheme) { + throw new Error(`Theme "${name}" does not have a source path for export`); + } + const customThemesDir = getCustomThemesDir(); + const themePath = path.join(customThemesDir, `${name}.json`); + if (!fs.existsSync(themePath)) { + throw new Error(`Theme not found: ${name}`); + } + const content = fs.readFileSync(themePath, "utf-8"); + return parseThemeJsonContent(name, content); +} + +function createTheme(themeJson: ThemeJson, mode?: ColorMode, sourcePath?: string): Theme { + const colorMode = mode ?? (getCapabilities().trueColor ? "truecolor" : "256color"); + const resolvedColors = resolveThemeColors(themeJson.colors, themeJson.vars); + const fgColors: Record = {} as Record; + const bgColors: Record = {} as Record; + const bgColorKeys: Set = new Set([ + "selectedBg", + "userMessageBg", + "customMessageBg", + "toolPendingBg", + "toolSuccessBg", + "toolErrorBg", + ]); + for (const [key, value] of Object.entries(resolvedColors)) { + if (bgColorKeys.has(key)) { + bgColors[key as ThemeBg] = value; + } else { + fgColors[key as ThemeColor] = value; + } + } + return new Theme(fgColors, bgColors, colorMode, { + name: themeJson.name, + sourcePath, + }); +} + +export function loadThemeFromPath(themePath: string, mode?: ColorMode): Theme { + const content = fs.readFileSync(themePath, "utf-8"); + const themeJson = parseThemeJsonContent(themePath, content); + return createTheme(themeJson, mode, themePath); +} + +function loadTheme(name: string, mode?: ColorMode): Theme { + const registeredTheme = registeredThemes.get(name); + if (registeredTheme) { + return registeredTheme; + } + const themeJson = loadThemeJson(name); + return createTheme(themeJson, mode); +} + +export function getThemeByName(name: string): Theme | undefined { + try { + return loadTheme(name); + } catch { + return undefined; + } +} + +export type TerminalTheme = "dark" | "light"; + +export interface RgbColor { + r: number; + g: number; + b: number; +} + +export interface TerminalThemeDetection { + theme: TerminalTheme; + source: "terminal background" | "COLORFGBG" | "fallback"; + detail: string; + confidence: "high" | "low"; +} + +export interface TerminalThemeDetectionOptions { + env?: NodeJS.ProcessEnv; +} + +function getColorFgBgBackgroundIndex(colorfgbg: string): number | undefined { + const parts = colorfgbg.split(";"); + for (let i = parts.length - 1; i >= 0; i--) { + const bg = Number.parseInt(parts[i].trim(), 10); + if (Number.isInteger(bg) && bg >= 0 && bg <= 255) { + return bg; + } + } + return undefined; +} + +function getRgbColorLuminance({ r, g, b }: RgbColor): number { + const toLinear = (channel: number) => { + const value = channel / 255; + return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4; + }; + return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b); +} + +function getAnsiColorLuminance(index: number): number { + return getRgbColorLuminance(hexToRgb(ansi256ToHex(index))); +} + +export function getThemeForRgbColor(rgb: RgbColor): TerminalTheme { + return getRgbColorLuminance(rgb) >= 0.5 ? "light" : "dark"; +} + +function parseOscHexChannel(channel: string): number | undefined { + if (!/^[0-9a-f]+$/i.test(channel)) { + return undefined; + } + const max = 16 ** channel.length - 1; + if (max <= 0) { + return undefined; + } + return Math.round((Number.parseInt(channel, 16) / max) * 255); +} + +export function parseOsc11BackgroundColor(data: string): RgbColor | undefined { + const prefix = "\u001B]11;"; + const belSuffix = "\u0007"; + const escSuffix = "\u001B\\"; + if (!data.startsWith(prefix)) { + return undefined; + } + + const suffixLength = data.endsWith(belSuffix) + ? belSuffix.length + : data.endsWith(escSuffix) + ? escSuffix.length + : 0; + if (suffixLength === 0) { + return undefined; + } + + const value = data.slice(prefix.length, -suffixLength).trim(); + if (value.includes("\u0007") || value.includes("\u001B")) { + return undefined; + } + if (value.startsWith("#")) { + const hex = value.slice(1); + if (/^[0-9a-f]{6}$/i.test(hex)) { + return hexToRgb(value); + } + if (/^[0-9a-f]{12}$/i.test(hex)) { + const r = parseOscHexChannel(hex.slice(0, 4)); + const g = parseOscHexChannel(hex.slice(4, 8)); + const b = parseOscHexChannel(hex.slice(8, 12)); + return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined; + } + return undefined; + } + + const rgbValue = value.replace(/^rgba?:/i, ""); + const [red, green, blue] = rgbValue.split("/"); + if (red === undefined || green === undefined || blue === undefined) { + return undefined; + } + const r = parseOscHexChannel(red); + const g = parseOscHexChannel(green); + const b = parseOscHexChannel(blue); + return r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined; +} + +export function detectTerminalBackground( + options: TerminalThemeDetectionOptions = {}, +): TerminalThemeDetection { + const env = options.env ?? process.env; + const colorfgbg = env.COLORFGBG || ""; + const bg = getColorFgBgBackgroundIndex(colorfgbg); + if (bg !== undefined) { + return { + theme: getAnsiColorLuminance(bg) >= 0.5 ? "light" : "dark", + source: "COLORFGBG", + detail: `background color index ${bg}`, + confidence: "high", + }; + } + + return { + theme: "dark", + source: "fallback", + detail: "no terminal background hint found", + confidence: "low", + }; +} + +export function getDefaultTheme(): string { + return detectTerminalBackground().theme; +} + +// ============================================================================ +// Global Theme Instance +// ============================================================================ + +// Use globalThis to share theme across module loaders (tsx + jiti in dev mode) +const THEME_KEY = Symbol.for("openclaw:agent-theme"); + +// Export theme as a getter that reads from globalThis +// This ensures all module instances (tsx, jiti) see the same theme +export const theme: Theme = new Proxy({} as Theme, { + get(_target, prop) { + const t = (globalThis as Record)[THEME_KEY]; + if (!t) { + throw new Error("Theme not initialized. Call initTheme() first."); + } + return (t as unknown as Record)[prop]; + }, +}); + +function setGlobalTheme(t: Theme): void { + (globalThis as Record)[THEME_KEY] = t; +} + +let currentThemeName: string | undefined; +let themeWatcher: fs.FSWatcher | undefined; +let themeReloadTimer: NodeJS.Timeout | undefined; +let onThemeChangeCallback: (() => void) | undefined; +const registeredThemes = new Map(); + +export function setRegisteredThemes(themes: Theme[]): void { + registeredThemes.clear(); + for (const theme of themes) { + if (theme.name) { + registeredThemes.set(theme.name, theme); + } + } +} + +export function initTheme(themeName?: string, enableWatcher: boolean = false): void { + const name = themeName ?? getDefaultTheme(); + currentThemeName = name; + try { + setGlobalTheme(loadTheme(name)); + if (enableWatcher) { + startThemeWatcher(); + } + } catch { + // Theme is invalid - fall back to dark theme silently + currentThemeName = "dark"; + setGlobalTheme(loadTheme("dark")); + // Don't start watcher for fallback theme + } +} + +export function setTheme( + name: string, + enableWatcher: boolean = false, +): { success: boolean; error?: string } { + currentThemeName = name; + try { + setGlobalTheme(loadTheme(name)); + if (enableWatcher) { + startThemeWatcher(); + } + if (onThemeChangeCallback) { + onThemeChangeCallback(); + } + return { success: true }; + } catch (error) { + // Theme is invalid - fall back to dark theme + currentThemeName = "dark"; + setGlobalTheme(loadTheme("dark")); + // Don't start watcher for fallback theme + return { + success: false, + error: error instanceof Error ? error.message : String(error), + }; + } +} + +export function setThemeInstance(themeInstance: Theme): void { + setGlobalTheme(themeInstance); + currentThemeName = ""; + stopThemeWatcher(); // Can't watch a direct instance + if (onThemeChangeCallback) { + onThemeChangeCallback(); + } +} + +export function onThemeChange(callback: () => void): void { + onThemeChangeCallback = callback; +} + +function startThemeWatcher(): void { + stopThemeWatcher(); + + // Only watch if it's a custom theme (not built-in) + if (!currentThemeName || currentThemeName === "dark" || currentThemeName === "light") { + return; + } + + const customThemesDir = getCustomThemesDir(); + const watchedThemeName = currentThemeName; + const watchedFileName = `${watchedThemeName}.json`; + const themeFile = path.join(customThemesDir, watchedFileName); + + // Only watch if the file exists + if (!fs.existsSync(themeFile)) { + return; + } + + const scheduleReload = () => { + if (themeReloadTimer) { + clearTimeout(themeReloadTimer); + } + themeReloadTimer = setTimeout(() => { + themeReloadTimer = undefined; + + // Ignore stale timers after switching themes or stopping the watcher + if (currentThemeName !== watchedThemeName) { + return; + } + + // Keep the last successfully loaded theme active if the file is temporarily missing + if (!fs.existsSync(themeFile)) { + return; + } + + try { + // Reload the theme from disk and refresh the registry cache + const reloadedTheme = loadThemeFromPath(themeFile); + registeredThemes.set(watchedThemeName, reloadedTheme); + setGlobalTheme(reloadedTheme); + // Notify callback (to invalidate UI) + if (onThemeChangeCallback) { + onThemeChangeCallback(); + } + } catch { + // Ignore errors (file might be in invalid state while being edited) + } + }, 100); + }; + + themeWatcher = + watchWithErrorHandler( + customThemesDir, + (_eventType, filename) => { + if (currentThemeName !== watchedThemeName) { + return; + } + if (!filename) { + scheduleReload(); + return; + } + if (filename !== watchedFileName) { + return; + } + scheduleReload(); + }, + () => { + closeWatcher(themeWatcher); + themeWatcher = undefined; + }, + ) ?? undefined; +} + +export function stopThemeWatcher(): void { + if (themeReloadTimer) { + clearTimeout(themeReloadTimer); + themeReloadTimer = undefined; + } + closeWatcher(themeWatcher); + themeWatcher = undefined; +} + +// ============================================================================ +// HTML Export Helpers +// ============================================================================ + +/** + * Convert a 256-color index to hex string. + * Indices 0-15: basic colors (approximate) + * Indices 16-231: 6x6x6 color cube + * Indices 232-255: grayscale ramp + */ +function ansi256ToHex(index: number): string { + // Basic colors (0-15) - approximate common terminal values + const basicColors = [ + "#000000", + "#800000", + "#008000", + "#808000", + "#000080", + "#800080", + "#008080", + "#c0c0c0", + "#808080", + "#ff0000", + "#00ff00", + "#ffff00", + "#0000ff", + "#ff00ff", + "#00ffff", + "#ffffff", + ]; + if (index < 16) { + return basicColors[index]; + } + + // Color cube (16-231): 6x6x6 = 216 colors + if (index < 232) { + const cubeIndex = index - 16; + const r = Math.floor(cubeIndex / 36); + const g = Math.floor((cubeIndex % 36) / 6); + const b = cubeIndex % 6; + const toHex = (n: number) => (n === 0 ? 0 : 55 + n * 40).toString(16).padStart(2, "0"); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + + // Grayscale (232-255): 24 shades + const gray = 8 + (index - 232) * 10; + const grayHex = gray.toString(16).padStart(2, "0"); + return `#${grayHex}${grayHex}${grayHex}`; +} + +/** + * Get resolved theme colors as CSS-compatible hex strings. + * Used by HTML export to generate CSS custom properties. + */ +export function getResolvedThemeColors(themeName?: string): Record { + const name = themeName ?? currentThemeName ?? getDefaultTheme(); + const isLight = name === "light"; + const themeJson = loadThemeJson(name); + const resolved = resolveThemeColors(themeJson.colors, themeJson.vars); + + // Default text color for empty values (terminal uses default fg color) + const defaultText = isLight ? "#000000" : "#e5e5e7"; + + const cssColors: Record = {}; + for (const [key, value] of Object.entries(resolved)) { + if (typeof value === "number") { + cssColors[key] = ansi256ToHex(value); + } else if (value === "") { + // Empty means default terminal color - use sensible fallback for HTML + cssColors[key] = defaultText; + } else { + cssColors[key] = value; + } + } + return cssColors; +} + +/** + * Check if a theme is a "light" theme (for CSS that needs light/dark variants). + */ +export function isLightTheme(themeName?: string): boolean { + // Currently just check the name - could be extended to analyze colors + return themeName === "light"; +} + +/** + * Get explicit export colors from theme JSON, if specified. + * Returns undefined for each color that isn't explicitly set. + */ +export function getThemeExportColors(themeName?: string): { + pageBg?: string; + cardBg?: string; + infoBg?: string; +} { + const name = themeName ?? currentThemeName ?? getDefaultTheme(); + try { + const themeJson = loadThemeJson(name); + const exportSection = themeJson.export; + if (!exportSection) { + return {}; + } + + const vars = themeJson.vars ?? {}; + const resolve = (value: ColorValue | undefined): string | undefined => { + if (value === undefined) { + return undefined; + } + const resolved = resolveVarRefs(value, vars); + if (typeof resolved === "number") { + return ansi256ToHex(resolved); + } + if (resolved === "") { + return undefined; + } + return resolved; + }; + + return { + pageBg: resolve(exportSection.pageBg), + cardBg: resolve(exportSection.cardBg), + infoBg: resolve(exportSection.infoBg), + }; + } catch { + return {}; + } +} + +// ============================================================================ +// TUI Helpers +// ============================================================================ + +type CliHighlightTheme = Record string>; + +let cachedHighlightThemeFor: Theme | undefined; +let cachedCliHighlightTheme: CliHighlightTheme | undefined; + +function buildCliHighlightTheme(t: Theme): CliHighlightTheme { + return { + keyword: (s: string) => t.fg("syntaxKeyword", s), + built_in: (s: string) => t.fg("syntaxType", s), + literal: (s: string) => t.fg("syntaxNumber", s), + number: (s: string) => t.fg("syntaxNumber", s), + string: (s: string) => t.fg("syntaxString", s), + comment: (s: string) => t.fg("syntaxComment", s), + function: (s: string) => t.fg("syntaxFunction", s), + title: (s: string) => t.fg("syntaxFunction", s), + class: (s: string) => t.fg("syntaxType", s), + type: (s: string) => t.fg("syntaxType", s), + attr: (s: string) => t.fg("syntaxVariable", s), + variable: (s: string) => t.fg("syntaxVariable", s), + params: (s: string) => t.fg("syntaxVariable", s), + operator: (s: string) => t.fg("syntaxOperator", s), + punctuation: (s: string) => t.fg("syntaxPunctuation", s), + }; +} + +function getCliHighlightTheme(t: Theme): CliHighlightTheme { + if (cachedHighlightThemeFor !== t || !cachedCliHighlightTheme) { + cachedHighlightThemeFor = t; + cachedCliHighlightTheme = buildCliHighlightTheme(t); + } + return cachedCliHighlightTheme; +} + +/** + * Highlight code with syntax coloring based on file extension or language. + * Returns array of highlighted lines. + */ +export function highlightCode(code: string, lang?: string): string[] { + // Validate language before highlighting to avoid stderr spam from cli-highlight + const validLang = lang && supportsLanguage(lang) ? lang : undefined; + // Skip highlighting when no valid language is specified. cli-highlight's + // auto-detection is unreliable and can misidentify prose as AppleScript, + // LiveCodeServer, etc., coloring random English words as keywords. + if (!validLang) { + return code.split("\n").map((line) => theme.fg("mdCodeBlock", line)); + } + const opts = { + language: validLang, + ignoreIllegals: true, + theme: getCliHighlightTheme(theme), + }; + try { + return highlight(code, opts).split("\n"); + } catch { + return code.split("\n"); + } +} + +/** + * Get language identifier from file path extension. + */ +export function getLanguageFromPath(filePath: string): string | undefined { + const ext = filePath.split(".").pop()?.toLowerCase(); + if (!ext) { + return undefined; + } + + const extToLang: Record = { + ts: "typescript", + tsx: "typescript", + js: "javascript", + jsx: "javascript", + mjs: "javascript", + cjs: "javascript", + py: "python", + rb: "ruby", + rs: "rust", + go: "go", + java: "java", + kt: "kotlin", + swift: "swift", + c: "c", + h: "c", + cpp: "cpp", + cc: "cpp", + cxx: "cpp", + hpp: "cpp", + cs: "csharp", + php: "php", + sh: "bash", + bash: "bash", + zsh: "bash", + fish: "fish", + ps1: "powershell", + sql: "sql", + html: "html", + htm: "html", + css: "css", + scss: "scss", + sass: "sass", + less: "less", + json: "json", + yaml: "yaml", + yml: "yaml", + toml: "toml", + xml: "xml", + md: "markdown", + markdown: "markdown", + dockerfile: "dockerfile", + makefile: "makefile", + cmake: "cmake", + lua: "lua", + perl: "perl", + r: "r", + scala: "scala", + clj: "clojure", + ex: "elixir", + exs: "elixir", + erl: "erlang", + hs: "haskell", + ml: "ocaml", + vim: "vim", + graphql: "graphql", + proto: "protobuf", + tf: "hcl", + hcl: "hcl", + }; + + return extToLang[ext]; +} + +export function getMarkdownTheme(): MarkdownTheme { + return { + heading: (text: string) => theme.fg("mdHeading", text), + link: (text: string) => theme.fg("mdLink", text), + linkUrl: (text: string) => theme.fg("mdLinkUrl", text), + code: (text: string) => theme.fg("mdCode", text), + codeBlock: (text: string) => theme.fg("mdCodeBlock", text), + codeBlockBorder: (text: string) => theme.fg("mdCodeBlockBorder", text), + quote: (text: string) => theme.fg("mdQuote", text), + quoteBorder: (text: string) => theme.fg("mdQuoteBorder", text), + hr: (text: string) => theme.fg("mdHr", text), + listBullet: (text: string) => theme.fg("mdListBullet", text), + bold: (text: string) => theme.bold(text), + italic: (text: string) => theme.italic(text), + underline: (text: string) => theme.underline(text), + strikethrough: (text: string) => chalk.strikethrough(text), + highlightCode: (code: string, lang?: string): string[] => { + // Validate language before highlighting to avoid stderr spam from cli-highlight + const validLang = lang && supportsLanguage(lang) ? lang : undefined; + // Skip highlighting when no valid language is specified. cli-highlight's + // auto-detection is unreliable and can misidentify prose as AppleScript, + // LiveCodeServer, etc., coloring random English words as keywords. + if (!validLang) { + return code.split("\n").map((line) => theme.fg("mdCodeBlock", line)); + } + const opts = { + language: validLang, + ignoreIllegals: true, + theme: getCliHighlightTheme(theme), + }; + try { + return highlight(code, opts).split("\n"); + } catch { + return code.split("\n").map((line) => theme.fg("mdCodeBlock", line)); + } + }, + }; +} + +export function getSelectListTheme(): SelectListTheme { + return { + selectedPrefix: (text: string) => theme.fg("accent", text), + selectedText: (text: string) => theme.fg("accent", text), + description: (text: string) => theme.fg("muted", text), + scrollInfo: (text: string) => theme.fg("muted", text), + noMatch: (text: string) => theme.fg("muted", text), + }; +} + +export function getEditorTheme(): EditorTheme { + return { + borderColor: (text: string) => theme.fg("borderMuted", text), + selectList: getSelectListTheme(), + }; +} + +export function getSettingsListTheme(): SettingsListTheme { + return { + label: (text: string, selected: boolean) => (selected ? theme.fg("accent", text) : text), + value: (text: string, selected: boolean) => + selected ? theme.fg("accent", text) : theme.fg("muted", text), + description: (text: string) => theme.fg("dim", text), + cursor: theme.fg("accent", "→ "), + hint: (text: string) => theme.fg("dim", text), + }; +} diff --git a/src/agents/moonshot.live.test.ts b/src/agents/moonshot.live.test.ts index 6e2d50d5989..aec925b271a 100644 --- a/src/agents/moonshot.live.test.ts +++ b/src/agents/moonshot.live.test.ts @@ -1,4 +1,4 @@ -import { completeSimple, type Model } from "@earendil-works/pi-ai"; +import { completeSimple, type Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { createSingleUserPromptMessage, diff --git a/src/agents/openai-codex-routing.test.ts b/src/agents/openai-codex-routing.test.ts index 530dc943857..43330ba0a4e 100644 --- a/src/agents/openai-codex-routing.test.ts +++ b/src/agents/openai-codex-routing.test.ts @@ -4,9 +4,8 @@ import { listOpenAIAuthProfileProvidersForAgentRuntime, modelSelectionShouldEnsureCodexPlugin, openAIProviderUsesCodexRuntimeByDefault, - resolveOpenAICompactionRuntimeProvider, - resolveOpenAIRuntimeProviderForPi, - resolveSelectedOpenAIPiRuntimeProvider, + resolveOpenAIRuntimeProvider, + resolveSelectedOpenAIRuntimeProvider, } from "./openai-codex-routing.js"; describe("OpenAI Codex routing policy", () => { @@ -36,24 +35,24 @@ describe("OpenAI Codex routing policy", () => { expect(modelSelectionShouldEnsureCodexPlugin({ model: "openai/gpt-5.5", config })).toBe(false); }); - it("maps explicit PI plus Codex auth profile to the legacy PI Codex-auth transport", () => { + it("maps explicit OpenClaw plus Codex auth profile to the OpenClaw Codex-auth transport", () => { expect( listOpenAIAuthProfileProvidersForAgentRuntime({ provider: "openai", - harnessRuntime: "pi", + harnessRuntime: "openclaw", }), ).toEqual(["openai", "openai-codex"]); expect( - resolveOpenAIRuntimeProviderForPi({ + resolveOpenAIRuntimeProvider({ provider: "openai", - harnessRuntime: "pi", + harnessRuntime: "openclaw", authProfileProvider: "openai-codex", authProfileId: "openai-codex:work", }), ).toBe("openai-codex"); }); - it("keeps explicit OpenAI PI Codex auth order ahead of API-key backups", () => { + it("keeps explicit OpenAI OpenClaw Codex auth order ahead of API-key backups", () => { const config = { auth: { order: { @@ -65,27 +64,27 @@ describe("OpenAI Codex routing policy", () => { expect( listOpenAIAuthProfileProvidersForAgentRuntime({ provider: "openai", - harnessRuntime: "pi", + harnessRuntime: "openclaw", config, }), ).toEqual(["openai-codex", "openai"]); expect( - resolveSelectedOpenAIPiRuntimeProvider({ + resolveSelectedOpenAIRuntimeProvider({ provider: "openai", - harnessRuntime: "pi", + harnessRuntime: "openclaw", config, }), ).toBe("openai-codex"); expect( - resolveOpenAIRuntimeProviderForPi({ + resolveOpenAIRuntimeProvider({ provider: "openai", - harnessRuntime: "pi", + harnessRuntime: "openclaw", config, }), ).toBe("openai"); }); - it("keeps explicit OpenAI PI API-key auth order ahead of Codex backups", () => { + it("keeps explicit OpenAI OpenClaw API-key auth order ahead of Codex backups", () => { const config = { auth: { order: { @@ -97,20 +96,20 @@ describe("OpenAI Codex routing policy", () => { expect( listOpenAIAuthProfileProvidersForAgentRuntime({ provider: "openai", - harnessRuntime: "pi", + harnessRuntime: "openclaw", config, }), ).toEqual(["openai", "openai-codex"]); expect( - resolveSelectedOpenAIPiRuntimeProvider({ + resolveSelectedOpenAIRuntimeProvider({ provider: "openai", - harnessRuntime: "pi", + harnessRuntime: "openclaw", config, }), ).toBe("openai"); }); - it("does not route custom OpenAI-compatible PI configs through Codex auth order", () => { + it("does not route custom OpenAI-compatible OpenClaw configs through Codex auth order", () => { const config = { models: { providers: { @@ -130,14 +129,14 @@ describe("OpenAI Codex routing policy", () => { expect( listOpenAIAuthProfileProvidersForAgentRuntime({ provider: "openai", - harnessRuntime: "pi", + harnessRuntime: "openclaw", config, }), ).toEqual(["openai", "openai-codex"]); expect( - resolveSelectedOpenAIPiRuntimeProvider({ + resolveSelectedOpenAIRuntimeProvider({ provider: "openai", - harnessRuntime: "pi", + harnessRuntime: "openclaw", config, }), ).toBe("openai"); @@ -152,80 +151,18 @@ describe("OpenAI Codex routing policy", () => { ).toEqual(["openai-codex"]); }); - it("routes selected OpenAI Codex runtime through OpenAI-Codex even before auth is configured", () => { + it("routes openai provider to openai-codex when harness runtime is codex", () => { expect( - resolveSelectedOpenAIPiRuntimeProvider({ + resolveSelectedOpenAIRuntimeProvider({ provider: "openai", harnessRuntime: "codex", }), ).toBe("openai-codex"); }); - it("routes OpenAI compaction to OpenAI-Codex when Codex auth order is configured", () => { - expect( - resolveOpenAICompactionRuntimeProvider({ - provider: "openai", - harnessRuntime: "codex", - config: { - auth: { - order: { - "openai-codex": ["openai-codex:work"], - }, - }, - }, - }), - ).toBe("openai-codex"); - }); - - it("routes OpenAI compaction to OpenAI-Codex when a Codex auth profile is configured", () => { - expect( - resolveOpenAICompactionRuntimeProvider({ - provider: "openai", - harnessRuntime: "codex", - config: { - auth: { - profiles: { - work: { - provider: "openai-codex", - mode: "oauth", - }, - }, - }, - }, - }), - ).toBe("openai-codex"); - }); - - it("routes OpenAI compaction to OpenAI-Codex when OpenAI auth order selects Codex", () => { - const config = { - auth: { - order: { - openai: ["openai-codex:work"], - }, - }, - } satisfies OpenClawConfig; - - expect( - resolveOpenAICompactionRuntimeProvider({ - provider: "openai", - harnessRuntime: "codex", - config, - }), - ).toBe("openai-codex"); - }); - - it("keeps OpenAI compaction on OpenAI when only direct API-key auth is implied", () => { - expect( - resolveOpenAICompactionRuntimeProvider({ - provider: "openai", - harnessRuntime: "codex", - }), - ).toBe("openai"); - }); - it("does not route non-OpenAI providers when runtime is codex", () => { expect( - resolveSelectedOpenAIPiRuntimeProvider({ + resolveSelectedOpenAIRuntimeProvider({ provider: "anthropic", harnessRuntime: "codex", }), diff --git a/src/agents/openai-codex-routing.ts b/src/agents/openai-codex-routing.ts index 553fa308cd7..dd91cc2ff94 100644 --- a/src/agents/openai-codex-routing.ts +++ b/src/agents/openai-codex-routing.ts @@ -1,6 +1,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; -import { normalizeEmbeddedAgentRuntime } from "./pi-embedded-runner/runtime.js"; +import { OPENCLAW_AGENT_RUNTIME_ID } from "./agent-runtime-id.js"; +import { normalizeOptionalAgentRuntimeId } from "./agent-runtime-id.js"; import { resolveProviderIdForAuth } from "./provider-auth-aliases.js"; import { findNormalizedProviderValue, normalizeProviderId } from "./provider-id.js"; @@ -92,27 +93,7 @@ function configuredOpenAIAuthOrderStartsWithCodexProfile(config: OpenClawConfig return hasOpenAICodexAuthProfileOverride(firstProfile); } -function configuredOpenAICodexAuthProfileExists(config: OpenClawConfig | undefined): boolean { - if (!openAIProviderUsesCodexRuntimeByDefault({ provider: OPENAI_PROVIDER_ID, config })) { - return false; - } - const configuredCodexOrder = findNormalizedProviderValue( - config?.auth?.order, - OPENAI_CODEX_PROVIDER_ID, - ); - if ( - configuredCodexOrder?.some( - (profileId) => typeof profileId === "string" && profileId.trim().length > 0, - ) === true - ) { - return true; - } - return Object.values(config?.auth?.profiles ?? {}).some( - (profile) => normalizeProviderId(profile?.provider ?? "") === OPENAI_CODEX_PROVIDER_ID, - ); -} - -export function shouldRouteOpenAIPiThroughCodexAuthProvider(params: { +export function shouldRouteOpenAIThroughCodexAuthProvider(params: { provider: string; harnessRuntime?: string; agentHarnessId?: string; @@ -124,8 +105,10 @@ export function shouldRouteOpenAIPiThroughCodexAuthProvider(params: { if (!isOpenAIProvider(params.provider)) { return false; } - const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime); - if (runtime !== "pi") { + const runtime = + normalizeOptionalAgentRuntimeId(params.agentHarnessId ?? params.harnessRuntime) ?? + OPENCLAW_AGENT_RUNTIME_ID; + if (runtime !== "openclaw") { return false; } if (!hasOpenAICodexAuthProfileOverride(params.authProfileId)) { @@ -151,13 +134,14 @@ export function listOpenAIAuthProfileProvidersForAgentRuntime(params: { if (!isOpenAIProvider(params.provider)) { return [params.provider]; } - const runtime = normalizeEmbeddedAgentRuntime( - normalizeExplicitRuntimePin(params.agentHarnessId) ?? params.harnessRuntime, - ); + const runtime = + normalizeOptionalAgentRuntimeId( + normalizeExplicitRuntimePin(params.agentHarnessId) ?? params.harnessRuntime, + ) ?? OPENCLAW_AGENT_RUNTIME_ID; if (runtime === "codex") { return [OPENAI_CODEX_PROVIDER_ID]; } - if (runtime === "pi") { + if (runtime === "openclaw") { if (configuredOpenAIAuthOrderStartsWithCodexProfile(params.config)) { return [OPENAI_CODEX_PROVIDER_ID, OPENAI_PROVIDER_ID]; } @@ -167,14 +151,11 @@ export function listOpenAIAuthProfileProvidersForAgentRuntime(params: { } function normalizeExplicitRuntimePin(value: unknown): string | undefined { - if (typeof value !== "string" || !value.trim()) { - return undefined; - } - const runtime = normalizeEmbeddedAgentRuntime(value); + const runtime = normalizeOptionalAgentRuntimeId(value); return runtime === "auto" || runtime === "default" ? undefined : runtime; } -export function resolveOpenAIRuntimeProviderForPi(params: { +export function resolveOpenAIRuntimeProvider(params: { provider: string; harnessRuntime?: string; agentHarnessId?: string; @@ -183,12 +164,12 @@ export function resolveOpenAIRuntimeProviderForPi(params: { config?: OpenClawConfig; workspaceDir?: string; }): string { - return shouldRouteOpenAIPiThroughCodexAuthProvider(params) + return shouldRouteOpenAIThroughCodexAuthProvider(params) ? OPENAI_CODEX_PROVIDER_ID : params.provider; } -export function resolveSelectedOpenAIPiRuntimeProvider(params: { +export function resolveSelectedOpenAIRuntimeProvider(params: { provider: string; harnessRuntime?: string; agentHarnessId?: string; @@ -197,48 +178,19 @@ export function resolveSelectedOpenAIPiRuntimeProvider(params: { config?: OpenClawConfig; workspaceDir?: string; }): string { - if (shouldRouteOpenAIPiThroughCodexAuthProvider(params)) { + if (shouldRouteOpenAIThroughCodexAuthProvider(params)) { return OPENAI_CODEX_PROVIDER_ID; } - const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime); + const runtime = + normalizeOptionalAgentRuntimeId(params.agentHarnessId ?? params.harnessRuntime) ?? + OPENCLAW_AGENT_RUNTIME_ID; if (!isOpenAIProvider(params.provider)) { return params.provider; } if (runtime === "codex") { return OPENAI_CODEX_PROVIDER_ID; } - return runtime === "pi" && - !params.authProfileId?.trim() && - configuredOpenAIAuthOrderStartsWithCodexProfile(params.config) - ? OPENAI_CODEX_PROVIDER_ID - : params.provider; -} - -export function resolveOpenAICompactionRuntimeProvider(params: { - provider: string; - harnessRuntime?: string; - agentHarnessId?: string; - authProfileProvider?: string; - authProfileId?: string; - config?: OpenClawConfig; - workspaceDir?: string; -}): string { - if (shouldRouteOpenAIPiThroughCodexAuthProvider(params)) { - return OPENAI_CODEX_PROVIDER_ID; - } - const runtime = normalizeEmbeddedAgentRuntime(params.agentHarnessId ?? params.harnessRuntime); - if (!isOpenAIProvider(params.provider)) { - return params.provider; - } - if ( - runtime === "codex" && - (hasOpenAICodexAuthProfileOverride(params.authProfileId) || - configuredOpenAIAuthOrderStartsWithCodexProfile(params.config) || - configuredOpenAICodexAuthProfileExists(params.config)) - ) { - return OPENAI_CODEX_PROVIDER_ID; - } - return runtime === "pi" && + return runtime === "openclaw" && !params.authProfileId?.trim() && configuredOpenAIAuthOrderStartsWithCodexProfile(params.config) ? OPENAI_CODEX_PROVIDER_ID @@ -250,7 +202,7 @@ export function resolveContextConfigProviderForRuntime(params: { runtimeId?: string; }): string { const provider = normalizeProviderId(params.provider); - const runtimeId = normalizeEmbeddedAgentRuntime(params.runtimeId); + const runtimeId = normalizeOptionalAgentRuntimeId(params.runtimeId) ?? OPENCLAW_AGENT_RUNTIME_ID; if (provider === OPENAI_PROVIDER_ID && runtimeId === "codex") { return OPENAI_CODEX_PROVIDER_ID; } diff --git a/src/agents/openai-completions-compat.ts b/src/agents/openai-completions-compat.ts index acfadd3e632..6716d20b3a5 100644 --- a/src/agents/openai-completions-compat.ts +++ b/src/agents/openai-completions-compat.ts @@ -1,4 +1,4 @@ -import type { Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import type { ProviderEndpointClass, ProviderRequestCapabilities } from "./provider-attribution.js"; import { resolveProviderRequestCapabilities } from "./provider-attribution.js"; diff --git a/src/agents/openai-reasoning-compat.live.test.ts b/src/agents/openai-reasoning-compat.live.test.ts index 1dc761b7841..079e72a43a6 100644 --- a/src/agents/openai-reasoning-compat.live.test.ts +++ b/src/agents/openai-reasoning-compat.live.test.ts @@ -1,10 +1,12 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { Api, Model } from "@earendil-works/pi-ai"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getRuntimeConfig } from "../config/config.js"; +import { discoverAuthStorage, discoverModels } from "./agent-model-discovery.js"; import { resolveDefaultAgentDir } from "./agent-scope.js"; +import { sanitizeSessionHistory } from "./embedded-agent-runner/replay-history.js"; import { completeSimpleWithTimeout, isLiveProfileKeyModeEnabled, @@ -15,8 +17,6 @@ import { } from "./live-test-helpers.js"; import { getApiKeyForModel, requireApiKey } from "./model-auth.js"; import { ensureOpenClawModelsJson } from "./models-config.js"; -import { sanitizeSessionHistory } from "./pi-embedded-runner/replay-history.js"; -import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js"; const LIVE = isLiveTestEnabled(); const REQUIRE_PROFILE_KEYS = isLiveProfileKeyModeEnabled(); @@ -28,7 +28,7 @@ const describeLive = LIVE ? describe : describe.skip; const logProgress = logLiveProgress; async function completeReplyWithRetry(params: { - model: Model; + model: Model; apiKey: string; message: string; }): Promise<{ text: string; errorMessage?: string }> { @@ -105,7 +105,7 @@ describeLive("openai reasoning compat live", () => { const agentDir = resolveDefaultAgentDir(cfg); const authStorage = discoverAuthStorage(agentDir); const modelRegistry = discoverModels(authStorage, agentDir); - const model = modelRegistry.find(provider, modelId) as Model | null; + const model = modelRegistry.find(provider, modelId) as Model | null; if (!model) { logProgress(`[openai-reasoning-compat] model missing from registry: ${TARGET_MODEL_REF}`); @@ -165,7 +165,7 @@ describeLive("openai reasoning compat live", () => { const agentDir = resolveDefaultAgentDir(cfg); const authStorage = discoverAuthStorage(agentDir); const modelRegistry = discoverModels(authStorage, agentDir); - const model = modelRegistry.find(provider, modelId) as Model | null; + const model = modelRegistry.find(provider, modelId) as Model | null; if (!model) { logProgress(`[openai-reasoning-compat] model missing from registry: ${TARGET_MODEL_REF}`); diff --git a/src/agents/openai-reasoning-effort.test.ts b/src/agents/openai-reasoning-effort.test.ts index 03018c7a94f..c49175443fb 100644 --- a/src/agents/openai-reasoning-effort.test.ts +++ b/src/agents/openai-reasoning-effort.test.ts @@ -25,7 +25,7 @@ describe("OpenAI reasoning effort support", () => { expect(resolveOpenAIReasoningEffortForModel({ model, effort: "medium" })).toBe("medium"); }); - it("does not downgrade xhigh when Pi compat metadata declares it explicitly", () => { + it("does not downgrade xhigh when model compat metadata declares it explicitly", () => { const model = { provider: "openai-codex", id: "gpt-5.5", diff --git a/src/agents/openai-responses-payload-policy.test.ts b/src/agents/openai-responses-payload-policy.test.ts index 75db8096681..4df11e8b4a0 100644 --- a/src/agents/openai-responses-payload-policy.test.ts +++ b/src/agents/openai-responses-payload-policy.test.ts @@ -1,4 +1,4 @@ -import type { Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { applyOpenAIResponsesPayloadPolicy, diff --git a/src/agents/openai-responses.reasoning-replay.test.ts b/src/agents/openai-responses.reasoning-replay.test.ts index 1dc5ab8cb68..fa7033914ab 100644 --- a/src/agents/openai-responses.reasoning-replay.test.ts +++ b/src/agents/openai-responses.reasoning-replay.test.ts @@ -1,5 +1,5 @@ -import type { AssistantMessage, Model, ToolResultMessage } from "@earendil-works/pi-ai"; -import { streamOpenAIResponses } from "@earendil-works/pi-ai"; +import type { AssistantMessage, Model, ToolResultMessage } from "openclaw/plugin-sdk/llm"; +import { streamOpenAIResponses } from "openclaw/plugin-sdk/llm"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; diff --git a/src/agents/openai-text-verbosity.ts b/src/agents/openai-text-verbosity.ts index 11cc76f4350..e39598323d1 100644 --- a/src/agents/openai-text-verbosity.ts +++ b/src/agents/openai-text-verbosity.ts @@ -1,5 +1,5 @@ import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js"; -import { log } from "./pi-embedded-runner/logger.js"; +import { log } from "./embedded-agent-runner/logger.js"; /** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ export type OpenAITextVerbosity = "low" | "medium" | "high"; diff --git a/src/agents/openai-thinking-contract.test.ts b/src/agents/openai-thinking-contract.test.ts index b19cdd170f1..adb2fd56713 100644 --- a/src/agents/openai-thinking-contract.test.ts +++ b/src/agents/openai-thinking-contract.test.ts @@ -1,13 +1,13 @@ -import { Agent, type StreamFn } from "@earendil-works/pi-agent-core"; +import { Agent, type StreamFn } from "openclaw/plugin-sdk/agent-core"; import { createAssistantMessageEventStream, type AssistantMessage, type Context, type Model, type SimpleStreamOptions, -} from "@earendil-works/pi-ai"; -import { streamSimpleOpenAICodexResponses } from "@earendil-works/pi-ai/openai-codex-responses"; -import { streamSimpleOpenAIResponses } from "@earendil-works/pi-ai/openai-responses"; +} from "openclaw/plugin-sdk/llm"; +import { streamSimpleOpenAICodexResponses } from "openclaw/plugin-sdk/llm-openai-codex-responses"; +import { streamSimpleOpenAIResponses } from "openclaw/plugin-sdk/llm-openai-responses"; import { describe, expect, it } from "vitest"; type ResponsesModel = Model<"openai-responses"> | Model<"openai-codex-responses">; @@ -40,7 +40,7 @@ describe("OpenAI thinking contract", () => { { model: openaiModel, expectedReasoning: "high" }, { model: codexModel, expectedReasoning: "high" }, ])( - "forwards enabled session thinkingLevel to pi-ai options for $model.provider/$model.id", + "forwards enabled session thinkingLevel to shared model runtime options for $model.provider/$model.id", async ({ model, expectedReasoning }) => { const capturedOptions: SimpleStreamOptions[] = []; const agent = new Agent({ @@ -75,7 +75,7 @@ describe("OpenAI thinking contract", () => { }, ); - it("serializes OpenAI Responses reasoning effort from pi-ai simple options", async () => { + it("serializes OpenAI Responses reasoning effort from shared model runtime simple options", async () => { const payload = await captureProviderPayload({ model: openaiModel, streamFn: streamSimpleOpenAIResponses, @@ -85,7 +85,7 @@ describe("OpenAI thinking contract", () => { expect(payload.reasoning).toEqual({ effort: "high", summary: "auto" }); }); - it("serializes Codex Responses reasoning effort from pi-ai simple options", async () => { + it("serializes Codex Responses reasoning effort from shared model runtime simple options", async () => { const payload = await captureProviderPayload({ model: codexModel, streamFn: streamSimpleOpenAICodexResponses, @@ -95,7 +95,7 @@ describe("OpenAI thinking contract", () => { expect(payload.reasoning).toEqual({ effort: "high", summary: "auto" }); }); - it("leaves Codex Responses reasoning absent when pi-agent-core disables thinking", async () => { + it("leaves Codex Responses reasoning absent when agent runtime disables thinking", async () => { const payload = await captureProviderPayload({ model: codexModel, streamFn: streamSimpleOpenAICodexResponses, @@ -105,7 +105,7 @@ describe("OpenAI thinking contract", () => { expect(payload).not.toHaveProperty("reasoning"); }); - it("keeps OpenAI Responses reasoning explicitly disabled when pi-agent-core disables thinking", async () => { + it("keeps OpenAI Responses reasoning explicitly disabled when agent runtime disables thinking", async () => { const payload = await captureProviderPayload({ model: openaiModel, streamFn: streamSimpleOpenAIResponses, diff --git a/src/agents/openai-tool-schema.ts b/src/agents/openai-tool-schema.ts index 14971a6f08b..a313816afda 100644 --- a/src/agents/openai-tool-schema.ts +++ b/src/agents/openai-tool-schema.ts @@ -1,5 +1,5 @@ import type { ModelCompatConfig } from "../config/types.models.js"; -import { normalizeToolParameterSchema } from "./pi-tools-parameter-schema.js"; +import { normalizeToolParameterSchema } from "./agent-tools-parameter-schema.js"; export { resolveOpenAIStrictToolSetting } from "./openai-strict-tool-setting.js"; type ToolSchemaCompatInput = { diff --git a/src/agents/openai-transport-stream.test.ts b/src/agents/openai-transport-stream.test.ts index 595cec56cdd..e410a1e6624 100644 --- a/src/agents/openai-transport-stream.test.ts +++ b/src/agents/openai-transport-stream.test.ts @@ -1,5 +1,5 @@ import { createServer } from "node:http"; -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; import { buildOpenAIResponsesParams, @@ -447,7 +447,7 @@ describe("openai transport stream", () => { ).toThrow(/Code mode payload tool surface violation/); }); - it("adds OpenClaw attribution to native OpenAI transport headers and protects it from pi", () => { + it("adds OpenClaw attribution to native OpenAI transport headers and protects it from provider overrides", () => { vi.stubEnv("OPENCLAW_VERSION", "2026.3.22"); const headers = testing.buildOpenAIClientHeaders( { @@ -457,8 +457,8 @@ describe("openai transport stream", () => { provider: "openai", baseUrl: "https://api.openai.com/v1", headers: { - originator: "pi", - "User-Agent": "pi", + originator: "openclaw", + "User-Agent": "openclaw", "X-Provider": "model", }, reasoning: true, @@ -469,8 +469,8 @@ describe("openai transport stream", () => { } satisfies Model<"openai-responses">, { systemPrompt: "", messages: [] } as never, { - originator: "pi", - "User-Agent": "pi", + originator: "openclaw", + "User-Agent": "openclaw", "X-Caller": "request", }, ); @@ -494,8 +494,8 @@ describe("openai transport stream", () => { provider: "openai-codex", baseUrl: "https://chatgpt.com/backend-api", headers: { - originator: "pi", - "User-Agent": "pi", + originator: "openclaw", + "User-Agent": "openclaw", }, reasoning: true, input: ["text"], @@ -3197,7 +3197,7 @@ describe("openai transport stream", () => { ]); }); - it("does not infer high reasoning when Pi passes thinking off", () => { + it("does not infer high reasoning when the runtime passes thinking off", () => { const params = buildOpenAIResponsesParams( { id: "gpt-5.4", diff --git a/src/agents/openai-transport-stream.ts b/src/agents/openai-transport-stream.ts index 74e4bbe4a5a..0b2988ca0fd 100644 --- a/src/agents/openai-transport-stream.ts +++ b/src/agents/openai-transport-stream.ts @@ -1,15 +1,4 @@ import { createHash, randomUUID } from "node:crypto"; -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { - calculateCost, - createAssistantMessageEventStream, - getEnvApiKey, - parseStreamingJson, - type Api, - type Context, - type Model, -} from "@earendil-works/pi-ai"; -import { convertMessages } from "@earendil-works/pi-ai/openai-completions"; import OpenAI, { AzureOpenAI } from "openai"; import type { ChatCompletionChunk } from "openai/resources/chat/completions.js"; import type { @@ -23,6 +12,16 @@ import type { ResponseOutputMessage, ResponseReasoningItem, } from "openai/resources/responses/responses.js"; +import { + calculateCost, + createAssistantMessageEventStream, + getEnvApiKey, + parseStreamingJson, + type Api, + type Context, + type Model, +} from "openclaw/plugin-sdk/llm"; +import { convertMessages } from "openclaw/plugin-sdk/llm-openai-completions"; import type { ModelCompatConfig } from "../config/types.models.js"; import { redactIdentifier } from "../logging/redact-identifier.js"; import { redactSensitiveText } from "../logging/redact.js"; @@ -72,6 +71,7 @@ import { resolveModelRequestTimeoutMs, } from "./provider-transport-fetch.js"; import { sanitizeResponsesImagePayload } from "./responses-image-payload-sanitizer.js"; +import type { StreamFn } from "./runtime/index.js"; import { stripSystemPromptCacheBoundary } from "./system-prompt-cache-boundary.js"; import { transformTransportMessages } from "./transport-message-transform.js"; import { @@ -120,7 +120,7 @@ type BaseStreamOptions = { cacheRetention?: "none" | "short" | "long"; sessionId?: string; authProfileId?: string; - onPayload?: (payload: unknown, model: Model) => unknown; + onPayload?: (payload: unknown, model: Model) => unknown; headers?: Record; openclawCodeModeToolSurface?: boolean; responseFormat?: Record; @@ -201,7 +201,7 @@ type OpenAIModeCompatInput = Omit & { thinkingFormat?: string; }; -type OpenAIModeModel = Omit, "compat"> & { +type OpenAIModeModel = Omit & { compat?: OpenAIModeCompatInput | null; }; @@ -612,7 +612,7 @@ function buildResponsesFailedFailureFields( function buildResponsesFailedNoDetailsObservation( event: Record, - model: Model, + model: Model, response: Record | undefined = isRecord(event.response) ? event.response : undefined, @@ -664,7 +664,7 @@ function summarizeResponsesFailedNoDetailsObservation( function normalizeResponsesFailedEvent( event: Record, - model: Model, + model: Model, ): ResponsesFailedEventSummary { const response = isRecord(event.response) ? event.response : undefined; const responseId = readResponseFailedString(response, "id") || undefined; @@ -820,7 +820,7 @@ function hashOptionalReplayContextValue(value: string | undefined): string | und } function buildOpenAIResponsesReplayContext( - model: Model, + model: Model, options?: Pick, ): OpenAIResponsesReplayContext { return { @@ -834,7 +834,7 @@ function buildOpenAIResponsesReplayContext( } function buildOpenAIResponsesReasoningReplayMetadata( - model: Model, + model: Model, options?: Pick, ): OpenAIResponsesReasoningReplayMetadata { return { @@ -846,7 +846,7 @@ function buildOpenAIResponsesReasoningReplayMetadata( function tagOpenAIResponsesReasoningReplayItem( item: Record, - model: Model, + model: Model, options?: Pick, ): Record { if (!("encrypted_content" in item)) { @@ -926,7 +926,7 @@ async function createResponsesStreamWithEncryptedContentRetry(params: { client: ResponsesClientLike; request: OpenAIResponsesRequestParams; requestOptions: unknown; - model: Model; + model: Model; }): Promise> { try { return (await params.client.responses.create( @@ -1004,7 +1004,7 @@ function parseTextSignature( } function convertResponsesMessages( - model: Model, + model: Model, context: Context, allowedToolCallProviders: Set, options?: { @@ -1041,7 +1041,7 @@ function convertResponsesMessages( }; const normalizeToolCallId = ( id: string, - _targetModel: Model, + _targetModel: Model, source: { provider: string; api: Api }, ) => { if (!allowedToolCallProviders.has(model.provider)) { @@ -1307,7 +1307,7 @@ function shouldLogOpenAIStrictToolDowngradeDiagnostic( return true; } -function createResponsesFirstEventTimeoutError(model: Model, timeoutMs: number): Error { +function createResponsesFirstEventTimeoutError(model: Model, timeoutMs: number): Error { return new Error( `Azure OpenAI Responses stream did not deliver a first event within ${timeoutMs}ms after HTTP streaming headers. ` + `provider=${model.provider} model=${model.id}. ` + @@ -1317,7 +1317,7 @@ function createResponsesFirstEventTimeoutError(model: Model, timeoutMs: num function withResponsesFirstEventTimeout( openaiStream: AsyncIterable, - model: Model, + model: Model, timeoutMs: number | undefined, ): AsyncIterable { if (timeoutMs === undefined || timeoutMs <= 0 || !Number.isFinite(timeoutMs)) { @@ -1366,7 +1366,7 @@ async function processResponsesStream( openaiStream: AsyncIterable, output: MutableAssistantOutput, stream: { push(event: unknown): void }, - model: Model, + model: Model, options?: { serviceTier?: ResponseCreateParamsStreaming["service_tier"]; applyServiceTierPricing?: ( @@ -1633,7 +1633,7 @@ function mapResponsesStopReason(status: string | undefined): string { } function buildOpenAIClientHeaders( - model: Model, + model: Model, context: Context, optionHeaders?: Record, turnHeaders?: Record, @@ -1663,7 +1663,7 @@ function buildOpenAIClientHeaders( } function resolveProviderTransportTurnState( - model: Model, + model: Model, params: { sessionId?: string; turnId: string; @@ -1685,17 +1685,17 @@ function resolveProviderTransportTurnState( }); } -function resolveOpenAISdkTimeoutMs(model: Model): number | undefined { +function resolveOpenAISdkTimeoutMs(model: Model): number | undefined { return resolveModelRequestTimeoutMs(model, undefined); } -function buildOpenAISdkClientOptions(model: Model): { timeout?: number } { +function buildOpenAISdkClientOptions(model: Model): { timeout?: number } { const timeout = resolveOpenAISdkTimeoutMs(model); return timeout === undefined ? {} : { timeout }; } function buildOpenAISdkRequestOptions( - model: Model, + model: Model, signal?: AbortSignal, ): { signal?: AbortSignal; timeout?: number } | undefined { const timeout = resolveOpenAISdkTimeoutMs(model); @@ -1709,7 +1709,7 @@ function buildOpenAISdkRequestOptions( } function createOpenAIResponsesClient( - model: Model, + model: Model, context: Context, apiKey: string, optionHeaders?: Record, @@ -1841,7 +1841,7 @@ function resolveCacheRetention(cacheRetention: string | undefined): "short" | "l if (cacheRetention === "short" || cacheRetention === "long" || cacheRetention === "none") { return cacheRetention; } - if (typeof process !== "undefined" && process.env.PI_CACHE_RETENTION === "long") { + if (typeof process !== "undefined" && process.env.OPENCLAW_CACHE_RETENTION === "long") { return "long"; } return "short"; @@ -1885,7 +1885,7 @@ function hasResponsesWebSearchTool(tools: unknown): boolean { } function raiseMinimalReasoningForResponsesWebSearch(params: { - model: Model; + model: Model; effort: OpenAIApiReasoningEffort; tools: unknown; }): OpenAIApiReasoningEffort { @@ -1936,7 +1936,7 @@ function isNativeOpenAICodexResponsesBaseUrl(baseUrl?: string): boolean { } } -function usesNativeOpenAICodexResponsesBackend(model: Model): boolean { +function usesNativeOpenAICodexResponsesBackend(model: Model): boolean { return isOpenAICodexResponsesModel(model) && isNativeOpenAICodexResponsesBaseUrl(model.baseUrl); } @@ -1964,7 +1964,7 @@ function stripOpenAICodexResponsesUnsupportedTextFields(params: Record>( - model: Model, + model: Model, params: T, ): T { if (!usesNativeOpenAICodexResponsesBackend(model)) { @@ -2018,7 +2018,7 @@ function resolveOpenAIResponsesTextFormat( } export function buildOpenAIResponsesParams( - model: Model, + model: Model, context: Context, options: OpenAIResponsesOptions | undefined, metadata?: Record, @@ -2243,7 +2243,7 @@ function normalizeAzureBaseUrl(baseUrl: string): string { return baseUrl.replace(/\/+$/, ""); } -function resolveAzureDeploymentName(model: Model): string { +function resolveAzureDeploymentName(model: Model): string { const deploymentMap = process.env.AZURE_OPENAI_DEPLOYMENT_NAME_MAP; if (deploymentMap) { for (const entry of deploymentMap.split(",")) { @@ -2257,7 +2257,7 @@ function resolveAzureDeploymentName(model: Model): string { } function createAzureOpenAIClient( - model: Model, + model: Model, context: Context, apiKey: string, optionHeaders?: Record, @@ -2275,7 +2275,7 @@ function createAzureOpenAIClient( } function buildAzureOpenAIResponsesParams( - model: Model, + model: Model, context: Context, options: OpenAIResponsesOptions | undefined, deploymentName: string, @@ -2309,7 +2309,7 @@ function assertOpenAICompletionsPayloadHasConversationTurn( } function createOpenAICompletionsClient( - model: Model, + model: Model, context: Context, apiKey: string, optionHeaders?: Record, @@ -2335,7 +2335,7 @@ function isAzureOpenAICompatibleHost(hostname: string): boolean { } function buildOpenAICompletionsClientConfig( - model: Model, + model: Model, context: Context, optionHeaders?: Record, ): { @@ -2453,7 +2453,7 @@ export function createOpenAICompletionsTransportStreamFn(): StreamFn { async function processOpenAICompletionsStream( responseStream: AsyncIterable, output: MutableAssistantOutput, - model: Model, + model: Model, stream: { push(event: unknown): void }, options?: { signal?: AbortSignal }, ) { @@ -3615,7 +3615,7 @@ export function buildOpenAICompletionsParams( export function parseTransportChunkUsage( rawUsage: NonNullable, - model: Model, + model: Model, ) { const cachedTokens = rawUsage.prompt_tokens_details?.cached_tokens || 0; const promptTokens = rawUsage.prompt_tokens || 0; diff --git a/src/agents/openclaw-owned-tool-runtime-contract.test.ts b/src/agents/openclaw-owned-tool-runtime-contract.test.ts index 3e73995b71c..1f275b6c3db 100644 --- a/src/agents/openclaw-owned-tool-runtime-contract.test.ts +++ b/src/agents/openclaw-owned-tool-runtime-contract.test.ts @@ -1,23 +1,23 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; -import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; +import type { AgentTool } from "openclaw/plugin-sdk/agent-core"; import { installOpenClawOwnedToolHooks, resetOpenClawOwnedToolHooks, textToolResult, } from "openclaw/plugin-sdk/agent-runtime-test-contracts"; +import type { ExtensionContext } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, describe, expect, it, vi } from "vitest"; -import type { MessagingToolSend } from "./pi-embedded-messaging.types.js"; +import { toToolDefinitions } from "./agent-tool-definition-adapter.js"; +import { createBaseToolHandlerState } from "./agent-tool-handler-state.test-helpers.js"; +import { wrapToolWithBeforeToolCallHook } from "./agent-tools.before-tool-call.js"; +import type { MessagingToolSend } from "./embedded-agent-messaging.types.js"; import { handleToolExecutionEnd, handleToolExecutionStart, -} from "./pi-embedded-subscribe.handlers.tools.js"; +} from "./embedded-agent-subscribe.handlers.tools.js"; import type { ToolCallSummary, ToolHandlerContext, -} from "./pi-embedded-subscribe.handlers.types.js"; -import { toToolDefinitions } from "./pi-tool-definition-adapter.js"; -import { createBaseToolHandlerState } from "./pi-tool-handler-state.test-helpers.js"; -import { wrapToolWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js"; +} from "./embedded-agent-subscribe.handlers.types.js"; function createContractTool(name: string, execute: AgentTool["execute"]): AgentTool { return { @@ -102,7 +102,7 @@ async function waitForAfterToolCall(hooks: { return call as [Record, Record]; } -describe("OpenClaw-owned tool runtime contract — Pi adapter", () => { +describe("OpenClaw-owned tool runtime contract - embedded agent adapter", () => { afterEach(() => { resetOpenClawOwnedToolHooks(); }); @@ -120,7 +120,7 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => { }); const definition = toToolDefinitions([tool])[0]; if (!definition) { - throw new Error("missing Pi tool definition"); + throw new Error("missing embedded agent tool definition"); } const ctx = createToolHandlerCtx(); const toolCallId = "call-contract"; @@ -168,7 +168,7 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => { expect(afterContext.toolCallId).toBe(toolCallId); }); - it("reports Pi dynamic tool execution errors through after_tool_call", async () => { + it("reports embedded agent dynamic tool execution errors through after_tool_call", async () => { const adjustedParams = { timeoutSec: 1 }; const mergedParams = { command: "false", timeoutSec: 1 }; const hooks = installOpenClawOwnedToolHooks({ adjustedParams }); @@ -183,7 +183,7 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => { }); const definition = toToolDefinitions([tool])[0]; if (!definition) { - throw new Error("missing Pi tool definition"); + throw new Error("missing embedded agent tool definition"); } const ctx = createToolHandlerCtx(); ctx.params.runId = "run-error"; @@ -229,7 +229,7 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => { expect(afterContext.toolCallId).toBe(toolCallId); }); - it("commits successful Pi messaging text, media, and target telemetry", async () => { + it("commits successful embedded agent messaging text, media, and target telemetry", async () => { const hooks = installOpenClawOwnedToolHooks(); const execute = vi.fn(async () => textToolResult("sent")); const tool = wrapToolWithBeforeToolCallHook(createContractTool("message", execute), { @@ -240,15 +240,15 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => { }); const definition = toToolDefinitions([tool])[0]; if (!definition) { - throw new Error("missing Pi tool definition"); + throw new Error("missing embedded agent tool definition"); } const ctx = createToolHandlerCtx(); ctx.params.runId = "run-message"; const toolCallId = "call-message"; const originalParams = { action: "send", - content: "hello from Pi", - mediaUrl: "/tmp/pi-reply.png", + content: "hello from embedded agent", + mediaUrl: "/tmp/openclaw-reply.png", provider: "telegram", to: "chat-1", }; @@ -278,8 +278,8 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => { }), ); - expect(ctx.state.messagingToolSentTexts).toEqual(["hello from Pi"]); - expect(ctx.state.messagingToolSentMediaUrls).toEqual(["/tmp/pi-reply.png"]); + expect(ctx.state.messagingToolSentTexts).toEqual(["hello from embedded agent"]); + expect(ctx.state.messagingToolSentMediaUrls).toEqual(["/tmp/openclaw-reply.png"]); expect( ctx.state.messagingToolSentTargets.map((target) => ({ tool: "message", @@ -293,8 +293,8 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => { tool: "message", provider: "telegram", to: "chat-1", - text: "hello from Pi", - mediaUrls: ["/tmp/pi-reply.png"], + text: "hello from embedded agent", + mediaUrls: ["/tmp/openclaw-reply.png"], }, ]); const [afterPayload, afterContext] = await waitForAfterToolCall(hooks); @@ -308,7 +308,7 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => { expect(afterContext.toolCallId).toBe(toolCallId); }); - it("fails closed when before_tool_call blocks a Pi dynamic tool", async () => { + it("fails closed when before_tool_call blocks an embedded agent dynamic tool", async () => { const hooks = installOpenClawOwnedToolHooks({ blockReason: "blocked by policy" }); const execute = vi.fn(async () => textToolResult("should not run")); const tool = wrapToolWithBeforeToolCallHook(createContractTool("message", execute), { @@ -319,7 +319,7 @@ describe("OpenClaw-owned tool runtime contract — Pi adapter", () => { }); const definition = toToolDefinitions([tool])[0]; if (!definition) { - throw new Error("missing Pi tool definition"); + throw new Error("missing embedded agent tool definition"); } const ctx = createToolHandlerCtx(); ctx.params.runId = "run-blocked"; diff --git a/src/agents/openclaw-tools.nodes-workspace-guard.ts b/src/agents/openclaw-tools.nodes-workspace-guard.ts index eee4a665772..c4a48257bd8 100644 --- a/src/agents/openclaw-tools.nodes-workspace-guard.ts +++ b/src/agents/openclaw-tools.nodes-workspace-guard.ts @@ -1,4 +1,4 @@ -import { wrapToolWorkspaceRootGuardWithOptions } from "./pi-tools.read.js"; +import { wrapToolWorkspaceRootGuardWithOptions } from "./agent-tools.read.js"; import type { ToolFsPolicy } from "./tool-fs-policy.js"; import type { AnyAgentTool } from "./tools/common.js"; diff --git a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts index 6a4bc27c061..9953befb895 100644 --- a/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts +++ b/src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts @@ -1,6 +1,10 @@ import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { AgentRouteBinding } from "../config/types.agents.js"; import { emitAgentEvent } from "../infra/agent-events.js"; +import { + testing as bundleMcpRuntimeTesting, + getOrCreateSessionMcpRuntime, +} from "./agent-bundle-mcp-tools.js"; import { getCallGatewayMock, getSessionsSpawnTool, @@ -13,10 +17,6 @@ import { setSessionsSpawnConfigOverride, waitForSessionsSpawnEvent, } from "./openclaw-tools.subagents.sessions-spawn.test-harness.js"; -import { - testing as bundleMcpRuntimeTesting, - getOrCreateSessionMcpRuntime, -} from "./pi-bundle-mcp-tools.js"; import { getLatestSubagentRunByChildSessionKey, resetSubagentRegistryForTests, diff --git a/src/agents/openclaw-tools.ts b/src/agents/openclaw-tools.ts index 71b8bc8db82..52023fa7f31 100644 --- a/src/agents/openclaw-tools.ts +++ b/src/agents/openclaw-tools.ts @@ -11,6 +11,11 @@ import { resolveTranscriptsConfig } from "../transcripts/config.js"; import { normalizeDeliveryContext } from "../utils/delivery-context.js"; import type { GatewayMessageChannel } from "../utils/message-channel.js"; import { resolveAgentWorkspaceDir, resolveSessionAgentIds } from "./agent-scope.js"; +import { + type HookContext, + isToolWrappedWithBeforeToolCallHook, + wrapToolWithBeforeToolCallHook, +} from "./agent-tools.before-tool-call.js"; import type { AuthProfileStore } from "./auth-profiles/types.js"; import { resolveOpenClawPluginToolsForOptions } from "./openclaw-plugin-tools.js"; import { @@ -24,11 +29,6 @@ import { collectPresentOpenClawTools, shouldIncludeUpdatePlanToolForOpenClawTools, } from "./openclaw-tools.registration.js"; -import { - type HookContext, - isToolWrappedWithBeforeToolCallHook, - wrapToolWithBeforeToolCallHook, -} from "./pi-tools.before-tool-call.js"; import type { SandboxFsBridge } from "./sandbox/fs-bridge.js"; import type { SpawnedToolContext } from "./spawned-context.js"; import type { ToolFsPolicy } from "./tool-fs-policy.js"; diff --git a/src/agents/openclaw-tools.update-plan.test.ts b/src/agents/openclaw-tools.update-plan.test.ts index 29cb1a19540..65e5c87c3c9 100644 --- a/src/agents/openclaw-tools.update-plan.test.ts +++ b/src/agents/openclaw-tools.update-plan.test.ts @@ -1,12 +1,12 @@ import { afterEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { setEmbeddedMode } from "../infra/embedded-mode.js"; +import { isToolWrappedWithBeforeToolCallHook } from "./agent-tools.before-tool-call.js"; import { createOpenClawTools } from "./openclaw-tools.js"; import { isUpdatePlanToolEnabledForOpenClawTools, shouldIncludeUpdatePlanToolForOpenClawTools, } from "./openclaw-tools.registration.js"; -import { isToolWrappedWithBeforeToolCallHook } from "./pi-tools.before-tool-call.js"; import { createUpdatePlanTool } from "./tools/update-plan-tool.js"; type UpdatePlanGatingParams = Parameters[0]; @@ -247,7 +247,7 @@ describe("openclaw-tools update_plan gating", () => { const cfg = { agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "default", }, }, @@ -276,7 +276,7 @@ describe("openclaw-tools update_plan gating", () => { const cfg = { agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "strict-agentic", }, }, @@ -291,7 +291,7 @@ describe("openclaw-tools update_plan gating", () => { const cfg = { agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "strict-agentic", }, }, @@ -315,7 +315,7 @@ describe("openclaw-tools update_plan gating", () => { }, agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "strict-agentic", }, }, @@ -330,7 +330,7 @@ describe("openclaw-tools update_plan gating", () => { const cfg = { agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "default", }, }, @@ -338,7 +338,7 @@ describe("openclaw-tools update_plan gating", () => { { id: "main" }, { id: "research", - embeddedPi: { + embeddedAgent: { executionContract: "strict-agentic", }, }, @@ -353,14 +353,14 @@ describe("openclaw-tools update_plan gating", () => { const cfg = { agents: { defaults: { - embeddedPi: { + embeddedAgent: { executionContract: "strict-agentic", }, }, list: [ { id: "main", - embeddedPi: { + embeddedAgent: { executionContract: "default", }, }, diff --git a/src/agents/outcome-fallback-runtime-contract.test.ts b/src/agents/outcome-fallback-runtime-contract.test.ts index a986cf2af71..f63323648e4 100644 --- a/src/agents/outcome-fallback-runtime-contract.test.ts +++ b/src/agents/outcome-fallback-runtime-contract.test.ts @@ -3,8 +3,9 @@ import { OUTCOME_FALLBACK_RUNTIME_CONTRACT, } from "openclaw/plugin-sdk/agent-runtime-test-contracts"; import { beforeAll, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { classifyEmbeddedAgentRunResultForModelFallback } from "./embedded-agent-runner/result-fallback-classifier.js"; import { runWithModelFallback } from "./model-fallback.js"; -import { classifyEmbeddedPiRunResultForModelFallback } from "./pi-embedded-runner/result-fallback-classifier.js"; vi.mock("./auth-profiles/source-check.js", () => ({ hasAnyAuthProfileStoreSource: () => false, @@ -14,7 +15,7 @@ const contractFallbackOverride = [ `${OUTCOME_FALLBACK_RUNTIME_CONTRACT.fallbackProvider}/${OUTCOME_FALLBACK_RUNTIME_CONTRACT.fallbackModel}`, ]; -describe("Outcome/fallback runtime contract - Pi fallback classifier", () => { +describe("Outcome/fallback runtime contract - embedded runtime fallback classifier", () => { beforeAll(async () => { await runWithModelFallback({ cfg: undefined, @@ -35,7 +36,7 @@ describe("Outcome/fallback runtime contract - Pi fallback classifier", () => { it.each(fallbackClassificationCases)( "maps harness classification %s to a format fallback code", (classification, code) => { - const fallback = classifyEmbeddedPiRunResultForModelFallback({ + const fallback = classifyEmbeddedAgentRunResultForModelFallback({ provider: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryProvider, model: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryModel, result: createContractRunResult({ @@ -73,7 +74,7 @@ describe("Outcome/fallback runtime contract - Pi fallback classifier", () => { fallbacksOverride: contractFallbackOverride, run, classifyResult: ({ provider, model, result }) => - classifyEmbeddedPiRunResultForModelFallback({ + classifyEmbeddedAgentRunResultForModelFallback({ provider, model, result, @@ -166,7 +167,7 @@ describe("Outcome/fallback runtime contract - Pi fallback classifier", () => { it("does not classify terminal results with visible output or side effects as fallbacks", () => { for (const contractCase of nonFallbackCases) { expect( - classifyEmbeddedPiRunResultForModelFallback({ + classifyEmbeddedAgentRunResultForModelFallback({ provider: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryProvider, model: OUTCOME_FALLBACK_RUNTIME_CONTRACT.primaryModel, result: contractCase.result, @@ -187,7 +188,7 @@ describe("Outcome/fallback runtime contract - Pi fallback classifier", () => { fallbacksOverride: contractFallbackOverride, run, classifyResult: ({ provider, model, result }) => - classifyEmbeddedPiRunResultForModelFallback({ + classifyEmbeddedAgentRunResultForModelFallback({ provider, model, result, diff --git a/src/agents/pi-embedded-runner.ts b/src/agents/pi-embedded-runner.ts deleted file mode 100644 index e8248dff642..00000000000 --- a/src/agents/pi-embedded-runner.ts +++ /dev/null @@ -1,41 +0,0 @@ -export { - compactEmbeddedPiSession, - compactEmbeddedPiSession as compactEmbeddedAgentSession, -} from "./pi-embedded-runner/compact.queued.js"; -export { applyExtraParamsToAgent } from "./pi-embedded-runner/extra-params.js"; - -export { resolveEmbeddedSessionLane } from "./pi-embedded-runner/lanes.js"; -export { - runEmbeddedPiAgent, - runEmbeddedPiAgent as runEmbeddedAgent, -} from "./pi-embedded-runner/run.js"; -export { - abortAndDrainEmbeddedPiRun, - abortEmbeddedPiRun, - abortEmbeddedPiRun as abortEmbeddedAgentRun, - isEmbeddedPiRunActive, - isEmbeddedPiRunActive as isEmbeddedAgentRunActive, - isEmbeddedPiRunStreaming, - isEmbeddedPiRunStreaming as isEmbeddedAgentRunStreaming, - queueEmbeddedPiMessage, - queueEmbeddedPiMessage as queueEmbeddedAgentMessage, - queueEmbeddedPiMessageWithOutcome, - resolveActiveEmbeddedRunSessionIdBySessionFile, - resolveActiveEmbeddedRunSessionId, - resolveActiveEmbeddedRunSessionId as resolveActiveEmbeddedAgentRunSessionId, - waitForEmbeddedPiRunEnd, - waitForEmbeddedPiRunEnd as waitForEmbeddedAgentRunEnd, -} from "./pi-embedded-runner/runs.js"; -export { buildEmbeddedSandboxInfo } from "./pi-embedded-runner/sandbox-info.js"; -export { createSystemPromptOverride } from "./pi-embedded-runner/system-prompt.js"; -export { splitSdkTools } from "./pi-embedded-runner/tool-split.js"; -export type { - EmbeddedPiAgentMeta as EmbeddedAgentMeta, - EmbeddedPiAgentMeta, - EmbeddedPiCompactResult as EmbeddedAgentCompactResult, - EmbeddedPiCompactResult, - EmbeddedPiRunMeta as EmbeddedAgentRunMeta, - EmbeddedPiRunMeta, - EmbeddedPiRunResult as EmbeddedAgentRunResult, - EmbeddedPiRunResult, -} from "./pi-embedded-runner/types.js"; diff --git a/src/agents/pi-embedded-runner/aliases.test.ts b/src/agents/pi-embedded-runner/aliases.test.ts deleted file mode 100644 index 272fa61d97b..00000000000 --- a/src/agents/pi-embedded-runner/aliases.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - abortEmbeddedAgentRun, - abortEmbeddedPiRun, - compactEmbeddedAgentSession, - compactEmbeddedPiSession, - runEmbeddedAgent, - runEmbeddedPiAgent, -} from "../pi-embedded-runner.js"; - -describe("embedded runner compatibility aliases", () => { - it("keeps neutral embedded-agent aliases bound to the PI compatibility exports", () => { - expect(runEmbeddedAgent).toBe(runEmbeddedPiAgent); - expect(compactEmbeddedAgentSession).toBe(compactEmbeddedPiSession); - expect(abortEmbeddedAgentRun).toBe(abortEmbeddedPiRun); - }); -}); diff --git a/src/agents/pi-embedded-runner/anthropic-cache-control-payload.test.ts b/src/agents/pi-embedded-runner/anthropic-cache-control-payload.test.ts deleted file mode 100644 index e17a9cda294..00000000000 --- a/src/agents/pi-embedded-runner/anthropic-cache-control-payload.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { applyAnthropicEphemeralCacheControlMarkers } from "./anthropic-cache-control-payload.js"; - -describe("applyAnthropicEphemeralCacheControlMarkers", () => { - it("marks system text content as ephemeral and strips thinking cache markers", () => { - const payload = { - messages: [ - { role: "system", content: "system prompt" }, - { - role: "assistant", - content: [ - { type: "thinking", text: "draft", cache_control: { type: "ephemeral" } }, - { type: "text", text: "answer" }, - ], - }, - ], - } satisfies Record; - - applyAnthropicEphemeralCacheControlMarkers(payload); - - expect(payload.messages).toEqual([ - { - role: "system", - content: [{ type: "text", text: "system prompt", cache_control: { type: "ephemeral" } }], - }, - { - role: "assistant", - content: [ - { type: "thinking", text: "draft" }, - { type: "text", text: "answer" }, - ], - }, - ]); - }); -}); diff --git a/src/agents/pi-embedded-runner/anthropic-cache-control-payload.ts b/src/agents/pi-embedded-runner/anthropic-cache-control-payload.ts deleted file mode 100644 index b06c10d1849..00000000000 --- a/src/agents/pi-embedded-runner/anthropic-cache-control-payload.ts +++ /dev/null @@ -1 +0,0 @@ -export { applyAnthropicEphemeralCacheControlMarkers } from "../anthropic-payload-policy.js"; diff --git a/src/agents/pi-embedded-runner/anthropic-family-cache-semantics.ts b/src/agents/pi-embedded-runner/anthropic-family-cache-semantics.ts deleted file mode 100644 index b700e752d0d..00000000000 --- a/src/agents/pi-embedded-runner/anthropic-family-cache-semantics.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - normalizeLowercaseStringOrEmpty, - normalizeOptionalLowercaseString, -} from "../../shared/string-coerce.js"; - -type AnthropicCacheRetentionFamily = - | "anthropic-direct" - | "anthropic-bedrock" - | "custom-anthropic-api"; - -export function isAnthropicModelRef(modelId: string): boolean { - return normalizeLowercaseStringOrEmpty(modelId).startsWith("anthropic/"); -} - -/** Matches Application Inference Profile ARNs across all AWS partitions with Bedrock. */ -const BEDROCK_APP_INFERENCE_PROFILE_ARN_RE = /^arn:aws(-cn|-us-gov)?:bedrock:/; - -export function isAnthropicBedrockModel(modelId: string): boolean { - const normalized = normalizeLowercaseStringOrEmpty(modelId); - - // Direct Anthropic Claude model IDs and regional inference profiles - // e.g. "anthropic.claude-sonnet-4-6", "us.anthropic.claude-sonnet-4-6", "global.anthropic.claude-opus-4-6-v1" - if (normalized.includes("anthropic.claude") || normalized.includes("anthropic/claude")) { - return true; - } - - // Application Inference Profile ARN — detect Claude via profile ID segment. - // e.g. "arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/my-claude-profile" - // - // Limitation: This is a name-heuristic only. Application inference profiles have - // user-defined names, so a profile named "my-prod-assistant" routing to Claude would - // miss cache semantics, while "my-claude-compat-llama" on a non-Claude model would - // incorrectly get them. The Bedrock API does not expose the underlying model in the - // profile ID itself — resolving this would require a GetInferenceProfile call, which - // is too expensive for a per-request check. System-defined profiles (us., eu., global.) - // always contain "anthropic.claude" and are matched above. - if ( - BEDROCK_APP_INFERENCE_PROFILE_ARN_RE.test(normalized) && - normalized.includes(":application-inference-profile/") - ) { - const profileId = normalized.split(":application-inference-profile/")[1] ?? ""; - return profileId.includes("claude"); - } - - return false; -} - -export function isOpenRouterAnthropicModelRef(provider: string, modelId: string): boolean { - return ( - normalizeOptionalLowercaseString(provider) === "openrouter" && isAnthropicModelRef(modelId) - ); -} - -export function isAnthropicFamilyCacheTtlEligible(params: { - provider: string; - modelApi?: string; - modelId: string; -}): boolean { - const normalizedProvider = normalizeOptionalLowercaseString(params.provider); - if (normalizedProvider === "anthropic" || normalizedProvider === "anthropic-vertex") { - return true; - } - if (normalizedProvider === "amazon-bedrock") { - return isAnthropicBedrockModel(params.modelId); - } - return params.modelApi === "anthropic-messages"; -} - -export function resolveAnthropicCacheRetentionFamily(params: { - provider: string; - modelApi?: string; - modelId?: string; - hasExplicitCacheConfig: boolean; -}): AnthropicCacheRetentionFamily | undefined { - const normalizedProvider = normalizeOptionalLowercaseString(params.provider); - if (normalizedProvider === "anthropic" || normalizedProvider === "anthropic-vertex") { - return "anthropic-direct"; - } - if ( - normalizedProvider === "amazon-bedrock" && - params.hasExplicitCacheConfig && - typeof params.modelId === "string" - ) { - if (isAnthropicBedrockModel(params.modelId)) { - return "anthropic-bedrock"; - } - // Application inference profiles with opaque IDs (e.g. z27qyso459da) can't - // be identified as Claude from the ARN alone. When the user explicitly sets - // cacheRetention, honor it — the extension's GetInferenceProfile resolution - // handles the actual model detection at runtime. - if ( - BEDROCK_APP_INFERENCE_PROFILE_ARN_RE.test(normalizeLowercaseStringOrEmpty(params.modelId)) && - normalizeLowercaseStringOrEmpty(params.modelId).includes(":application-inference-profile/") - ) { - return "anthropic-bedrock"; - } - } - if ( - normalizedProvider !== "amazon-bedrock" && - params.hasExplicitCacheConfig && - params.modelApi === "anthropic-messages" - ) { - return "custom-anthropic-api"; - } - return undefined; -} diff --git a/src/agents/pi-embedded-runner/anthropic-family-tool-payload-compat.ts b/src/agents/pi-embedded-runner/anthropic-family-tool-payload-compat.ts deleted file mode 100644 index d1970061e3d..00000000000 --- a/src/agents/pi-embedded-runner/anthropic-family-tool-payload-compat.ts +++ /dev/null @@ -1,172 +0,0 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { streamSimple } from "@earendil-works/pi-ai"; -import { normalizeOptionalString } from "../../shared/string-coerce.js"; -type AnthropicToolSchemaMode = "openai-functions"; -type AnthropicToolChoiceMode = "openai-string-modes"; - -type AnthropicToolPayloadCompatibilityOptions = { - toolSchemaMode?: AnthropicToolSchemaMode; - toolChoiceMode?: AnthropicToolChoiceMode; -}; - -function hasOpenAiAnthropicToolPayloadCompatFlag(model: { compat?: unknown }): boolean { - if (!model.compat || typeof model.compat !== "object" || Array.isArray(model.compat)) { - return false; - } - - return ( - (model.compat as { requiresOpenAiAnthropicToolPayload?: unknown }) - .requiresOpenAiAnthropicToolPayload === true - ); -} - -function requiresAnthropicToolPayloadCompatibilityForModel( - model: { - api?: unknown; - compat?: unknown; - }, - options?: AnthropicToolPayloadCompatibilityOptions, -): boolean { - if (model.api !== "anthropic-messages") { - return false; - } - return ( - Boolean(options?.toolSchemaMode || options?.toolChoiceMode) || - hasOpenAiAnthropicToolPayloadCompatFlag(model) - ); -} - -function usesOpenAiFunctionAnthropicToolSchemaForModel( - model: { - compat?: unknown; - }, - options?: AnthropicToolPayloadCompatibilityOptions, -): boolean { - return ( - options?.toolSchemaMode === "openai-functions" || hasOpenAiAnthropicToolPayloadCompatFlag(model) - ); -} - -function usesOpenAiStringModeAnthropicToolChoiceForModel( - model: { - compat?: unknown; - }, - options?: AnthropicToolPayloadCompatibilityOptions, -): boolean { - return ( - options?.toolChoiceMode === "openai-string-modes" || - hasOpenAiAnthropicToolPayloadCompatFlag(model) - ); -} - -function normalizeOpenAiFunctionAnthropicToolDefinition( - tool: unknown, -): Record | undefined { - if (!tool || typeof tool !== "object" || Array.isArray(tool)) { - return undefined; - } - - const toolObj = tool as Record; - if (toolObj.function && typeof toolObj.function === "object") { - return toolObj; - } - - const rawName = normalizeOptionalString(toolObj.name) ?? ""; - if (!rawName) { - return toolObj; - } - - const functionSpec: Record = { - name: rawName, - parameters: - toolObj.input_schema && typeof toolObj.input_schema === "object" - ? toolObj.input_schema - : toolObj.parameters && typeof toolObj.parameters === "object" - ? toolObj.parameters - : { type: "object", properties: {} }, - }; - - if (typeof toolObj.description === "string" && toolObj.description.trim()) { - functionSpec.description = toolObj.description; - } - if (typeof toolObj.strict === "boolean") { - functionSpec.strict = toolObj.strict; - } - - return { - type: "function", - function: functionSpec, - }; -} - -function normalizeOpenAiStringModeAnthropicToolChoice(toolChoice: unknown): unknown { - if (!toolChoice || typeof toolChoice !== "object" || Array.isArray(toolChoice)) { - return toolChoice; - } - - const choice = toolChoice as Record; - if (choice.type === "auto") { - return "auto"; - } - if (choice.type === "none") { - return "none"; - } - if (choice.type === "required" || choice.type === "any") { - return "required"; - } - if (choice.type === "tool" && typeof choice.name === "string" && choice.name.trim()) { - return { - type: "function", - function: { name: choice.name.trim() }, - }; - } - - return toolChoice; -} - -/** @deprecated Anthropic-family provider stream helper; do not use from third-party plugins. */ -export function createAnthropicToolPayloadCompatibilityWrapper( - baseStreamFn: StreamFn | undefined, - options?: AnthropicToolPayloadCompatibilityOptions, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, streamOptions) => { - const originalOnPayload = streamOptions?.onPayload; - return underlying(model, context, { - ...streamOptions, - onPayload: (payload) => { - if ( - payload && - typeof payload === "object" && - requiresAnthropicToolPayloadCompatibilityForModel(model, options) - ) { - const payloadObj = payload as Record; - if ( - Array.isArray(payloadObj.tools) && - usesOpenAiFunctionAnthropicToolSchemaForModel(model, options) - ) { - payloadObj.tools = payloadObj.tools - .map((tool) => normalizeOpenAiFunctionAnthropicToolDefinition(tool)) - .filter((tool): tool is Record => !!tool); - } - if (usesOpenAiStringModeAnthropicToolChoiceForModel(model, options)) { - payloadObj.tool_choice = normalizeOpenAiStringModeAnthropicToolChoice( - payloadObj.tool_choice, - ); - } - } - return originalOnPayload?.(payload, model); - }, - }); - }; -} - -/** @deprecated Anthropic-family provider stream helper; do not use from third-party plugins. */ -export function createOpenAIAnthropicToolPayloadCompatibilityWrapper( - baseStreamFn: StreamFn | undefined, -): StreamFn { - return createAnthropicToolPayloadCompatibilityWrapper(baseStreamFn, { - toolSchemaMode: "openai-functions", - toolChoiceMode: "openai-string-modes", - }); -} diff --git a/src/agents/pi-embedded-runner/compact.runtime.ts b/src/agents/pi-embedded-runner/compact.runtime.ts deleted file mode 100644 index eeb9137c796..00000000000 --- a/src/agents/pi-embedded-runner/compact.runtime.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createLazyImportLoader } from "../../shared/lazy-promise.js"; -import type { CompactEmbeddedPiSessionDirect } from "./compact.runtime.types.js"; - -const compactRuntimeLoader = createLazyImportLoader(() => import("./compact.js")); - -function loadCompactRuntime() { - return compactRuntimeLoader.load(); -} - -export async function compactEmbeddedPiSessionDirect( - ...args: Parameters -): ReturnType { - const { compactEmbeddedPiSessionDirect } = await loadCompactRuntime(); - return compactEmbeddedPiSessionDirect(...args); -} diff --git a/src/agents/pi-embedded-runner/compact.runtime.types.ts b/src/agents/pi-embedded-runner/compact.runtime.types.ts deleted file mode 100644 index 1d554dae461..00000000000 --- a/src/agents/pi-embedded-runner/compact.runtime.types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { CompactEmbeddedPiSessionParams } from "./compact.types.js"; -import type { EmbeddedPiCompactResult } from "./types.js"; - -export type CompactEmbeddedPiSessionDirect = ( - params: CompactEmbeddedPiSessionParams, -) => Promise; diff --git a/src/agents/pi-embedded-runner/google-stream-wrappers.test.ts b/src/agents/pi-embedded-runner/google-stream-wrappers.test.ts deleted file mode 100644 index 44556ff6b8f..00000000000 --- a/src/agents/pi-embedded-runner/google-stream-wrappers.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { sanitizeGoogleThinkingPayload } from "./google-stream-wrappers.js"; - -describe("sanitizeGoogleThinkingPayload — gemini-2.5-pro zero budget", () => { - it("removes thinkingBudget=0 for gemini-2.5-pro", () => { - const payload = { - config: { - thinkingConfig: { thinkingBudget: 0 }, - }, - }; - sanitizeGoogleThinkingPayload({ payload, modelId: "gemini-2.5-pro" }); - expect(payload.config).not.toHaveProperty("thinkingConfig"); - }); - - it("removes thinkingBudget=0 for gemini-2.5-pro with provider prefix", () => { - const payload = { - config: { - thinkingConfig: { thinkingBudget: 0 }, - }, - }; - sanitizeGoogleThinkingPayload({ payload, modelId: "google/gemini-2.5-pro-preview" }); - expect(payload.config).not.toHaveProperty("thinkingConfig"); - }); - - it("removes only thinkingBudget and preserves other thinkingConfig keys", () => { - const payload = { - config: { - thinkingConfig: { thinkingBudget: 0, includeThoughts: true }, - }, - }; - sanitizeGoogleThinkingPayload({ payload, modelId: "gemini-2.5-pro" }); - expect(payload.config.thinkingConfig).not.toHaveProperty("thinkingBudget"); - expect(payload.config.thinkingConfig).toHaveProperty("includeThoughts", true); - }); - - it("removes thinkingBudget=0 from native Google generationConfig payloads", () => { - const payload = { - generationConfig: { - thinkingConfig: { thinkingBudget: 0, includeThoughts: true }, - }, - }; - sanitizeGoogleThinkingPayload({ payload, modelId: "gemini-2.5-pro" }); - expect(payload.generationConfig.thinkingConfig).not.toHaveProperty("thinkingBudget"); - expect(payload.generationConfig.thinkingConfig).toHaveProperty("includeThoughts", true); - }); - - it("keeps thinkingBudget=0 for gemini-2.5-flash (not thinking-required)", () => { - const payload = { - config: { - thinkingConfig: { thinkingBudget: 0 }, - }, - }; - sanitizeGoogleThinkingPayload({ payload, modelId: "gemini-2.5-flash" }); - expect(payload.config.thinkingConfig).toHaveProperty("thinkingBudget", 0); - }); - - it("keeps positive thinkingBudget for gemini-2.5-pro", () => { - const payload = { - config: { - thinkingConfig: { thinkingBudget: 1000 }, - }, - }; - sanitizeGoogleThinkingPayload({ payload, modelId: "gemini-2.5-pro" }); - expect(payload.config.thinkingConfig).toHaveProperty("thinkingBudget", 1000); - }); - - it("rewrites Gemini 3 Pro budgets to thinkingLevel", () => { - const payload = { - config: { - thinkingConfig: { thinkingBudget: 2048, includeThoughts: true }, - }, - }; - sanitizeGoogleThinkingPayload({ - payload, - modelId: "gemini-3.1-pro-preview", - thinkingLevel: "high", - }); - expect(payload.config.thinkingConfig).toEqual({ - includeThoughts: true, - thinkingLevel: "HIGH", - }); - }); - - it("rewrites Gemini 3 Flash latest disabled budgets to minimal thinkingLevel", () => { - const payload = { - generationConfig: { - thinkingConfig: { thinkingBudget: 0 }, - }, - }; - sanitizeGoogleThinkingPayload({ - payload, - modelId: "gemini-flash-latest", - thinkingLevel: "off", - }); - expect(payload.generationConfig.thinkingConfig).toEqual({ - thinkingLevel: "MINIMAL", - }); - }); - - it("rewrites Gemini 3 Flash negative budgets when a fixed thinking level is explicit", () => { - const payload = { - config: { - thinkingConfig: { thinkingBudget: -1, includeThoughts: true }, - }, - }; - sanitizeGoogleThinkingPayload({ - payload, - modelId: "gemini-3-flash-preview", - thinkingLevel: "medium", - }); - expect(payload.config.thinkingConfig).toEqual({ - includeThoughts: true, - thinkingLevel: "MEDIUM", - }); - }); - - it("keeps Gemini 3 adaptive thinking on provider dynamic defaults", () => { - const payload = { - config: { - thinkingConfig: { thinkingBudget: 8192, includeThoughts: true }, - }, - }; - sanitizeGoogleThinkingPayload({ - payload, - modelId: "gemini-3-flash-preview", - thinkingLevel: "adaptive", - }); - expect(payload.config.thinkingConfig).toEqual({ - includeThoughts: true, - }); - }); - - it("maps Gemini 2.5 adaptive thinking to thinkingBudget=-1", () => { - const payload = { - config: { - thinkingConfig: { thinkingBudget: 8192, includeThoughts: true }, - }, - }; - sanitizeGoogleThinkingPayload({ - payload, - modelId: "gemini-2.5-flash", - thinkingLevel: "adaptive", - }); - expect(payload.config.thinkingConfig).toEqual({ - includeThoughts: true, - thinkingBudget: -1, - }); - }); -}); diff --git a/src/agents/pi-embedded-runner/google-stream-wrappers.ts b/src/agents/pi-embedded-runner/google-stream-wrappers.ts deleted file mode 100644 index 64573ccfb1c..00000000000 --- a/src/agents/pi-embedded-runner/google-stream-wrappers.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - createGoogleThinkingPayloadWrapper, - sanitizeGoogleThinkingPayload, -} from "../../plugin-sdk/provider-stream-shared.js"; diff --git a/src/agents/pi-embedded-runner/minimax-stream-wrappers.test.ts b/src/agents/pi-embedded-runner/minimax-stream-wrappers.test.ts deleted file mode 100644 index 1b515f529b0..00000000000 --- a/src/agents/pi-embedded-runner/minimax-stream-wrappers.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { Context, Model } from "@earendil-works/pi-ai"; -import { describe, expect, it } from "vitest"; -import { - createMinimaxFastModeWrapper, - createMinimaxThinkingDisabledWrapper, -} from "./minimax-stream-wrappers.js"; - -function captureThinkingPayload(params: { - provider: string; - api: string; - modelId: string; -}): unknown { - let capturedThinking: unknown = undefined; - const baseStreamFn: StreamFn = (_model, _context, options) => { - const payload: Record = {}; - options?.onPayload?.(payload, _model); - capturedThinking = payload.thinking; - return {} as ReturnType; - }; - - const wrapped = createMinimaxThinkingDisabledWrapper(baseStreamFn); - void wrapped( - { - api: params.api, - provider: params.provider, - id: params.modelId, - } as Model<"anthropic-messages">, - { messages: [] } as Context, - {}, - ); - - return capturedThinking; -} - -describe("createMinimaxThinkingDisabledWrapper", () => { - it("disables thinking for minimax anthropic-messages provider", () => { - expect( - captureThinkingPayload({ - provider: "minimax", - api: "anthropic-messages", - modelId: "MiniMax-M2.7", - }), - ).toEqual({ type: "disabled" }); - }); - - it("disables thinking for minimax-portal anthropic-messages provider", () => { - expect( - captureThinkingPayload({ - provider: "minimax-portal", - api: "anthropic-messages", - modelId: "MiniMax-M2.7", - }), - ).toEqual({ type: "disabled" }); - }); - - it("does not affect non-minimax providers", () => { - expect( - captureThinkingPayload({ - provider: "anthropic", - api: "anthropic-messages", - modelId: "claude-sonnet-4-6", - }), - ).toBeUndefined(); - }); - - it("does not affect minimax with non-anthropic-messages api", () => { - expect( - captureThinkingPayload({ - provider: "minimax", - api: "openai-completions", - modelId: "MiniMax-M2.7", - }), - ).toBeUndefined(); - }); - - it("preserves an already-set thinking value", () => { - let capturedThinking: unknown = undefined; - const baseStreamFn: StreamFn = (_model, _context, options) => { - const payload: Record = { - thinking: { type: "enabled", budget_tokens: 1024 }, - }; - options?.onPayload?.(payload, _model); - capturedThinking = payload.thinking; - return {} as ReturnType; - }; - - const wrapped = createMinimaxThinkingDisabledWrapper(baseStreamFn); - void wrapped( - { - api: "anthropic-messages", - provider: "minimax", - id: "MiniMax-M2.7", - } as Model<"anthropic-messages">, - { messages: [] } as Context, - {}, - ); - - expect(capturedThinking).toEqual({ type: "enabled", budget_tokens: 1024 }); - }); -}); - -describe("createMinimaxFastModeWrapper", () => { - it("rewrites MiniMax-M2.7 to highspeed variant in fast mode", () => { - let capturedId = ""; - const baseStreamFn: StreamFn = (model) => { - capturedId = model.id; - return {} as ReturnType; - }; - - const wrapped = createMinimaxFastModeWrapper(baseStreamFn, true); - void wrapped( - { - api: "anthropic-messages", - provider: "minimax", - id: "MiniMax-M2.7", - } as Model<"anthropic-messages">, - { messages: [] } as Context, - {}, - ); - - expect(capturedId).toBe("MiniMax-M2.7-highspeed"); - }); -}); diff --git a/src/agents/pi-embedded-runner/minimax-stream-wrappers.ts b/src/agents/pi-embedded-runner/minimax-stream-wrappers.ts deleted file mode 100644 index cd216daf4e6..00000000000 --- a/src/agents/pi-embedded-runner/minimax-stream-wrappers.ts +++ /dev/null @@ -1,78 +0,0 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { streamSimple } from "@earendil-works/pi-ai"; - -const MINIMAX_FAST_MODEL_IDS = new Map([ - ["MiniMax-M2.7", "MiniMax-M2.7-highspeed"], -]); - -function resolveMinimaxFastModelId(modelId: unknown): string | undefined { - if (typeof modelId !== "string") { - return undefined; - } - return MINIMAX_FAST_MODEL_IDS.get(modelId.trim()); -} - -function isMinimaxAnthropicMessagesModel(model: { api?: unknown; provider?: unknown }): boolean { - return ( - model.api === "anthropic-messages" && - (model.provider === "minimax" || model.provider === "minimax-portal") - ); -} - -/** @deprecated MiniMax provider-owned stream helper; do not use from third-party plugins. */ -export function createMinimaxFastModeWrapper( - baseStreamFn: StreamFn | undefined, - fastMode: boolean, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - if ( - !fastMode || - model.api !== "anthropic-messages" || - (model.provider !== "minimax" && model.provider !== "minimax-portal") - ) { - return underlying(model, context, options); - } - - const fastModelId = resolveMinimaxFastModelId(model.id); - if (!fastModelId) { - return underlying(model, context, options); - } - - return underlying({ ...model, id: fastModelId }, context, options); - }; -} - -/** - * MiniMax's Anthropic-compatible streaming endpoint returns reasoning_content - * in OpenAI-style delta chunks ({delta: {content: "", reasoning_content: "..."}}) - * rather than the native Anthropic thinking block format. Pi-ai's Anthropic - * provider cannot process this format and leaks the reasoning text as visible - * content. Disable thinking in the outgoing payload so MiniMax does not produce - * reasoning_content deltas during streaming. - */ -/** @deprecated MiniMax provider-owned stream helper; do not use from third-party plugins. */ -export function createMinimaxThinkingDisabledWrapper(baseStreamFn: StreamFn | undefined): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - if (!isMinimaxAnthropicMessagesModel(model)) { - return underlying(model, context, options); - } - - const originalOnPayload = options?.onPayload; - return underlying(model, context, { - ...options, - onPayload: (payload) => { - if (payload && typeof payload === "object") { - const payloadObj = payload as Record; - // Only inject if thinking is not already explicitly set. - // This preserves any intentional override from other wrappers. - if (payloadObj.thinking === undefined) { - payloadObj.thinking = { type: "disabled" }; - } - } - return originalOnPayload?.(payload, model); - }, - }); - }; -} diff --git a/src/agents/pi-embedded-runner/model-context-tokens.ts b/src/agents/pi-embedded-runner/model-context-tokens.ts deleted file mode 100644 index 31d2e601564..00000000000 --- a/src/agents/pi-embedded-runner/model-context-tokens.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; -import { asFiniteNumber } from "../../shared/number-coercion.js"; - -type PiModelWithOptionalContextTokens = Model & { - contextTokens?: number; -}; - -export function readPiModelContextTokens(model: Model | null | undefined): number | undefined { - const value = (model as PiModelWithOptionalContextTokens | null | undefined)?.contextTokens; - return asFiniteNumber(value); -} diff --git a/src/agents/pi-embedded-runner/model.provider-normalization.ts b/src/agents/pi-embedded-runner/model.provider-normalization.ts deleted file mode 100644 index f73b85ff2e2..00000000000 --- a/src/agents/pi-embedded-runner/model.provider-normalization.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; -import { normalizeModelCompat } from "../../plugins/provider-model-compat.js"; - -export function normalizeResolvedProviderModel(params: { - provider: string; - model: Model; -}): Model { - return normalizeModelCompat(params.model); -} diff --git a/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts b/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts deleted file mode 100644 index 9d08febff76..00000000000 --- a/src/agents/pi-embedded-runner/moonshot-stream-wrappers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { streamSimple } from "@earendil-works/pi-ai"; -import type { ThinkLevel } from "../../auto-reply/thinking.js"; -import { streamWithPayloadPatch } from "./stream-payload-utils.js"; - -export { - createMoonshotThinkingWrapper, - resolveMoonshotThinkingKeep, - resolveMoonshotThinkingType, -} from "./moonshot-thinking-stream-wrappers.js"; - -export function shouldApplySiliconFlowThinkingOffCompat(params: { - provider: string; - modelId: string; - thinkingLevel?: ThinkLevel; -}): boolean { - return ( - params.provider === "siliconflow" && - params.thinkingLevel === "off" && - params.modelId.startsWith("Pro/") - ); -} - -export function createSiliconFlowThinkingWrapper(baseStreamFn: StreamFn | undefined): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => - streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { - if (payloadObj.thinking === "off") { - payloadObj.thinking = null; - } - }); -} diff --git a/src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts b/src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts deleted file mode 100644 index e29464b92df..00000000000 --- a/src/agents/pi-embedded-runner/moonshot-thinking-stream-wrappers.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { ThinkLevel } from "../../auto-reply/thinking.js"; -import { createLazyImportLoader } from "../../shared/lazy-promise.js"; -import { normalizeOptionalLowercaseString } from "../../shared/string-coerce.js"; -import { streamWithPayloadPatch } from "./stream-payload-utils.js"; - -type MoonshotThinkingType = "enabled" | "disabled"; -type MoonshotThinkingKeep = "all"; -const MOONSHOT_THINKING_KEEP_MODEL_ID = "kimi-k2.6"; -const piAiRuntimeLoader = createLazyImportLoader(() => import("@earendil-works/pi-ai")); - -async function loadDefaultStreamFn(): Promise { - const runtime = await piAiRuntimeLoader.load(); - return runtime.streamSimple; -} - -function normalizeMoonshotThinkingType(value: unknown): MoonshotThinkingType | undefined { - if (typeof value === "boolean") { - return value ? "enabled" : "disabled"; - } - if (typeof value === "string") { - const normalized = normalizeOptionalLowercaseString(value); - if (!normalized) { - return undefined; - } - if (["enabled", "enable", "on", "true"].includes(normalized)) { - return "enabled"; - } - if (["disabled", "disable", "off", "false"].includes(normalized)) { - return "disabled"; - } - return undefined; - } - if (value && typeof value === "object" && !Array.isArray(value)) { - return normalizeMoonshotThinkingType((value as Record).type); - } - return undefined; -} - -function normalizeMoonshotThinkingKeep(value: unknown): MoonshotThinkingKeep | undefined { - if (!value || typeof value !== "object" || Array.isArray(value)) { - return undefined; - } - const keepValue = (value as Record).keep; - if (typeof keepValue !== "string") { - return undefined; - } - return normalizeOptionalLowercaseString(keepValue) === "all" ? "all" : undefined; -} - -function isMoonshotToolChoiceCompatible(toolChoice: unknown): boolean { - if (toolChoice == null || toolChoice === "auto" || toolChoice === "none") { - return true; - } - if (typeof toolChoice === "object" && !Array.isArray(toolChoice)) { - const typeValue = (toolChoice as Record).type; - return typeValue === "auto" || typeValue === "none"; - } - return false; -} - -function isPinnedToolChoice(toolChoice: unknown): boolean { - if (!toolChoice || typeof toolChoice !== "object" || Array.isArray(toolChoice)) { - return false; - } - const typeValue = (toolChoice as Record).type; - return typeValue === "tool" || typeValue === "function"; -} - -/** @deprecated Moonshot provider-owned stream helper; do not use from third-party plugins. */ -export function resolveMoonshotThinkingType(params: { - configuredThinking: unknown; - thinkingLevel?: ThinkLevel; -}): MoonshotThinkingType | undefined { - const configured = normalizeMoonshotThinkingType(params.configuredThinking); - if (configured) { - return configured; - } - if (!params.thinkingLevel) { - return undefined; - } - return params.thinkingLevel === "off" ? "disabled" : "enabled"; -} - -/** @deprecated Moonshot provider-owned stream helper; do not use from third-party plugins. */ -export function resolveMoonshotThinkingKeep(params: { - configuredThinking: unknown; -}): MoonshotThinkingKeep | undefined { - return normalizeMoonshotThinkingKeep(params.configuredThinking); -} - -/** @deprecated Moonshot provider-owned stream helper; do not use from third-party plugins. */ -export function createMoonshotThinkingWrapper( - baseStreamFn: StreamFn | undefined, - thinkingType?: MoonshotThinkingType, - thinkingKeep?: MoonshotThinkingKeep, -): StreamFn { - return async (model, context, options) => { - const underlying = baseStreamFn ?? (await loadDefaultStreamFn()); - return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { - let effectiveThinkingType = normalizeMoonshotThinkingType(payloadObj.thinking); - - if (thinkingType) { - payloadObj.thinking = { type: thinkingType }; - effectiveThinkingType = thinkingType; - } - - if ( - effectiveThinkingType === "enabled" && - !isMoonshotToolChoiceCompatible(payloadObj.tool_choice) - ) { - if (payloadObj.tool_choice === "required") { - payloadObj.tool_choice = "auto"; - } else if (isPinnedToolChoice(payloadObj.tool_choice)) { - payloadObj.thinking = { type: "disabled" }; - effectiveThinkingType = "disabled"; - } - } - - // thinking.keep is only valid on kimi-k2.6 when thinking is enabled. Gate - // by the final payload.model and final type so stray config never leaks. - const isKeepCapableModel = payloadObj.model === MOONSHOT_THINKING_KEEP_MODEL_ID; - if (payloadObj.thinking && typeof payloadObj.thinking === "object") { - const thinkingObj = payloadObj.thinking as Record; - if (isKeepCapableModel && effectiveThinkingType === "enabled" && thinkingKeep === "all") { - thinkingObj.keep = "all"; - } else if ("keep" in thinkingObj) { - delete thinkingObj.keep; - } - } - }); - }; -} diff --git a/src/agents/pi-embedded-runner/openai-stream-wrappers.test.ts b/src/agents/pi-embedded-runner/openai-stream-wrappers.test.ts deleted file mode 100644 index 4db5c133d3e..00000000000 --- a/src/agents/pi-embedded-runner/openai-stream-wrappers.test.ts +++ /dev/null @@ -1,585 +0,0 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { Model } from "@earendil-works/pi-ai"; -import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; -import { describe, expect, it } from "vitest"; -import { - createOpenAIAttributionHeadersWrapper, - createOpenAICompletionsStrictMessageKeysWrapper, - createOpenAICompletionsToolsCompatWrapper, - createOpenAIThinkingLevelWrapper, - createCodexNativeWebSearchWrapper, -} from "./openai-stream-wrappers.js"; - -function createPayloadCapture(opts?: { initialReasoning?: unknown }) { - const payloads: Array> = []; - const baseStreamFn: StreamFn = (model, _context, options) => { - const payload: Record = { model: model.id }; - if (opts?.initialReasoning !== undefined) { - payload.reasoning = structuredClone(opts.initialReasoning); - } - options?.onPayload?.(payload, model); - payloads.push(structuredClone(payload)); - return createAssistantMessageEventStream(); - }; - return { baseStreamFn, payloads }; -} - -const codexModel = { - api: "openai-codex-responses", - provider: "openai-codex", - id: "gpt-5.1-codex", -} as Model<"openai-codex-responses">; - -const openaiModel = { - api: "openai-responses", - provider: "openai", - id: "gpt-5.2", -} as Model<"openai-responses">; - -describe("createOpenAICompletionsToolsCompatWrapper", () => { - it("strips tools fields when OpenAI-compatible models disable tool support", () => { - const payloads: Array> = []; - const baseStreamFn: StreamFn = (model, _context, options) => { - const payload: Record = { - model: model.id, - tools: [{ type: "function", function: { name: "noop" } }], - tool_choice: "auto", - parallel_tool_calls: true, - }; - options?.onPayload?.(payload, model); - payloads.push(structuredClone(payload)); - return createAssistantMessageEventStream(); - }; - - const wrapped = createOpenAICompletionsToolsCompatWrapper(baseStreamFn); - void wrapped( - { - api: "openai-completions", - provider: "venice", - id: "chat-only-model", - baseUrl: "https://example.invalid/v1", - compat: { supportsTools: false }, - } as unknown as Model<"openai-completions">, - { messages: [] }, - {}, - ); - - expect(payloads[0]).not.toHaveProperty("tools"); - expect(payloads[0]).not.toHaveProperty("tool_choice"); - expect(payloads[0]).not.toHaveProperty("parallel_tool_calls"); - }); - - it("keeps tools fields for OpenAI-compatible models without an explicit opt-out", () => { - const payloads: Array> = []; - const baseStreamFn: StreamFn = (model, _context, options) => { - const payload: Record = { - model: model.id, - tools: [{ type: "function", function: { name: "noop" } }], - }; - options?.onPayload?.(payload, model); - payloads.push(structuredClone(payload)); - return createAssistantMessageEventStream(); - }; - - const wrapped = createOpenAICompletionsToolsCompatWrapper(baseStreamFn); - void wrapped( - { - api: "openai-completions", - provider: "venice", - id: "tool-capable-model", - baseUrl: "https://example.invalid/v1", - } as Model<"openai-completions">, - { messages: [] }, - {}, - ); - - expect(payloads[0]).toHaveProperty("tools"); - }); -}); - -describe("createCodexNativeWebSearchWrapper", () => { - it("does not inject native web_search when code mode owns the tool surface", () => { - const payloads: Array> = []; - const baseStreamFn: StreamFn = (model, _context, options) => { - const payload: Record = { - model: model.id, - tools: [ - { type: "function", name: "exec" }, - { type: "function", name: "wait" }, - { type: "function", name: "web_search" }, - { type: "web_search" }, - ], - }; - options?.onPayload?.(payload, model); - payloads.push(structuredClone(payload)); - return createAssistantMessageEventStream(); - }; - const wrapped = createCodexNativeWebSearchWrapper(baseStreamFn, { - config: { - tools: { - codeMode: { enabled: true }, - web: { - search: { - enabled: true, - openaiCodex: { enabled: true, mode: "cached" }, - }, - }, - }, - }, - }); - - void wrapped( - { - api: "openai-codex-responses", - provider: "gateway", - id: "gpt-5.5", - } as Model<"openai-codex-responses">, - { - messages: [], - tools: [ - { name: "exec", description: "", parameters: {} }, - { name: "wait", description: "", parameters: {} }, - ], - }, - { - onPayload: (payload) => { - const payloadObj = payload as { tools?: unknown } | undefined; - if (payloadObj && Array.isArray(payloadObj.tools)) { - payloadObj.tools.push({ type: "function", name: "web_search" }); - } - }, - }, - ); - - expect(payloads[0]?.tools).toEqual([ - { type: "function", name: "exec" }, - { type: "function", name: "wait" }, - ]); - }); - - it("does not enable code-mode transport enforcement when config is on but controls are inactive", () => { - const observedOptions: Array> = []; - const payloads: Array> = []; - const baseStreamFn: StreamFn = (model, _context, options) => { - observedOptions.push(options as Record); - const payload: Record = { model: model.id }; - options?.onPayload?.(payload, model); - payloads.push(structuredClone(payload)); - return createAssistantMessageEventStream(); - }; - const wrapped = createCodexNativeWebSearchWrapper(baseStreamFn, { - config: { - tools: { - codeMode: { enabled: true }, - }, - }, - }); - - void wrapped( - { - api: "openai-codex-responses", - provider: "gateway", - id: "gpt-5.5", - } as Model<"openai-codex-responses">, - { messages: [] }, - {}, - ); - - expect(observedOptions[0]?.openclawCodeModeToolSurface).toBeUndefined(); - expect(payloads[0]).toEqual({ model: "gpt-5.5" }); - }); - - it("enforces the code-mode transport surface when the run enables it at agent scope", () => { - const observedOptions: Array> = []; - const payloads: Array> = []; - const baseStreamFn: StreamFn = (model, _context, options) => { - observedOptions.push(options as Record); - const payload: Record = { - model: model.id, - tools: [ - { type: "function", name: "exec" }, - { type: "function", name: "wait" }, - { type: "function", name: "read" }, - ], - }; - options?.onPayload?.(payload, model); - payloads.push(structuredClone(payload)); - return createAssistantMessageEventStream(); - }; - const wrapped = createCodexNativeWebSearchWrapper(baseStreamFn, { - codeModeToolSurfaceEnabled: true, - }); - - void wrapped( - { - api: "openai-codex-responses", - provider: "gateway", - id: "gpt-5.5", - } as Model<"openai-codex-responses">, - { - messages: [], - tools: [ - { name: "exec", description: "", parameters: {} }, - { name: "wait", description: "", parameters: {} }, - ], - }, - {}, - ); - - expect(observedOptions[0]?.openclawCodeModeToolSurface).toBe(true); - expect(payloads[0]?.tools).toEqual([ - { type: "function", name: "exec" }, - { type: "function", name: "wait" }, - ]); - }); - - it("keeps grouped provider tool declarations when code mode filters the payload", () => { - const payloads: Array> = []; - const baseStreamFn: StreamFn = (model, _context, options) => { - const payload: Record = { - model: model.id, - tools: [ - { - functionDeclarations: [ - { name: "exec", description: "Run code" }, - { name: "read", description: "Read a file" }, - { name: "wait", description: "Resume code" }, - ], - }, - { google_search: {} }, - ], - }; - options?.onPayload?.(payload, model); - payloads.push(structuredClone(payload)); - return createAssistantMessageEventStream(); - }; - const wrapped = createCodexNativeWebSearchWrapper(baseStreamFn, { - codeModeToolSurfaceEnabled: true, - }); - - void wrapped( - { - api: "google-generative-ai", - provider: "google", - id: "gemini-3.1-pro", - } as never, - { - messages: [], - tools: [ - { name: "exec", description: "", parameters: {} }, - { name: "wait", description: "", parameters: {} }, - ], - }, - {}, - ); - - expect(payloads[0]?.tools).toEqual([ - { - functionDeclarations: [ - { name: "exec", description: "Run code" }, - { name: "wait", description: "Resume code" }, - ], - }, - ]); - }); -}); - -describe("createOpenAICompletionsStrictMessageKeysWrapper", () => { - it("strips message keys to role and content for strict OpenAI-compatible endpoints", () => { - const payloads: Array> = []; - const baseStreamFn: StreamFn = (model, _context, options) => { - const payload: Record = { - model: model.id, - messages: [ - { - role: "assistant", - content: "calling tool", - name: "agent", - tool_calls: [{ id: "call_1", type: "function", function: { name: "noop" } }], - cache_control: { type: "ephemeral" }, - }, - { - role: "tool", - content: "tool result", - tool_call_id: "call_1", - }, - ], - }; - options?.onPayload?.(payload, model); - payloads.push(structuredClone(payload)); - return createAssistantMessageEventStream(); - }; - - const wrapped = createOpenAICompletionsStrictMessageKeysWrapper(baseStreamFn); - void wrapped( - { - api: "openai-completions", - provider: "infomaniak", - id: "mistral3", - baseUrl: "https://api.infomaniak.com/1/ai/example/openai", - compat: { strictMessageKeys: true }, - } as unknown as Model<"openai-completions">, - { messages: [] }, - {}, - ); - - expect(payloads[0]?.messages).toEqual([ - { role: "assistant", content: "calling tool" }, - { role: "tool", content: "tool result" }, - ]); - }); -}); - -describe("createOpenAIThinkingLevelWrapper", () => { - it("overrides effort on reasoning-capable model when thinkingLevel is medium", () => { - const { baseStreamFn, payloads } = createPayloadCapture({ - initialReasoning: { effort: "none" }, - }); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "medium"); - void wrapped(codexModel, { messages: [] }, {}); - - expect(payloads[0]?.reasoning).toEqual({ effort: "medium" }); - }); - - it("overrides effort on reasoning-capable model when thinkingLevel is high", () => { - const { baseStreamFn, payloads } = createPayloadCapture({ - initialReasoning: { effort: "none" }, - }); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "high"); - void wrapped(openaiModel, { messages: [] }, {}); - - expect(payloads[0]?.reasoning).toEqual({ effort: "high" }); - }); - - it("removes reasoning when thinkingLevel is off on reasoning-capable model", () => { - const { baseStreamFn, payloads } = createPayloadCapture({ - initialReasoning: { effort: "medium" }, - }); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "off"); - void wrapped(codexModel, { messages: [] }, {}); - - expect(payloads[0]).not.toHaveProperty("reasoning"); - }); - - it("maps adaptive thinkingLevel to medium effort on reasoning-capable model", () => { - const { baseStreamFn, payloads } = createPayloadCapture({ - initialReasoning: { effort: "none" }, - }); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "adaptive"); - void wrapped(codexModel, { messages: [] }, {}); - - expect(payloads[0]?.reasoning).toEqual({ effort: "medium" }); - }); - - it("replaces string disabled reasoning when thinkingLevel is enabled", () => { - const { baseStreamFn, payloads } = createPayloadCapture({ initialReasoning: "none" }); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "low"); - void wrapped(codexModel, { messages: [] }, {}); - - expect(payloads[0]?.reasoning).toEqual({ effort: "low" }); - }); - - it("does not add reasoning for non-reasoning models without existing reasoning payload", () => { - const { baseStreamFn, payloads } = createPayloadCapture(); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "medium"); - void wrapped(openaiModel, { messages: [] }, {}); - - expect(payloads[0]?.reasoning).toBeUndefined(); - }); - - it("overrides existing reasoning.effort from upstream wrappers", () => { - const { baseStreamFn, payloads } = createPayloadCapture({ - initialReasoning: { effort: "none" }, - }); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "medium"); - void wrapped(codexModel, { messages: [] }, {}); - - expect(payloads[0]?.reasoning).toEqual({ effort: "medium" }); - }); - - it("returns underlying streamFn unchanged when thinkingLevel is undefined", () => { - const { baseStreamFn } = createPayloadCapture(); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, undefined); - expect(wrapped).toBe(baseStreamFn); - }); - - it("preserves other reasoning properties when overriding effort", () => { - const { baseStreamFn, payloads } = createPayloadCapture({ - initialReasoning: { effort: "none", summary: "auto" }, - }); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "high"); - void wrapped(codexModel, { messages: [] }, {}); - - expect(payloads[0]?.reasoning).toEqual({ effort: "high", summary: "auto" }); - }); - - it("does not inject reasoning for completions API on proxy routes", () => { - const { baseStreamFn, payloads } = createPayloadCapture(); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "medium"); - void wrapped( - { - api: "openai-completions", - provider: "openai", - id: "gpt-4o", - baseUrl: "https://proxy.example.com/v1", - } as Model<"openai-completions">, - { messages: [] }, - {}, - ); - - expect(payloads[0]?.reasoning).toBeUndefined(); - }); - - it("does not inject reasoning for proxy routes with custom baseUrl", () => { - const { baseStreamFn, payloads } = createPayloadCapture(); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "medium"); - void wrapped( - { - api: "openai-responses", - provider: "openai", - id: "gpt-5.2", - baseUrl: "https://proxy.example.com/v1", - } as Model<"openai-responses">, - { messages: [] }, - {}, - ); - - expect(payloads[0]?.reasoning).toBeUndefined(); - }); - - it("passes through all thinking levels correctly on reasoning-capable models", () => { - const levels = ["minimal", "low", "medium", "high", "xhigh"] as const; - for (const level of levels) { - const { baseStreamFn, payloads } = createPayloadCapture({ - initialReasoning: { effort: "none" }, - }); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, level); - void wrapped(codexModel, { messages: [] }, {}); - expect(payloads[0]?.reasoning).toEqual({ effort: level }); - } - }); - - it("raises minimal reasoning for web_search on loopback Responses routes", () => { - const payloads: Array> = []; - const baseStreamFn: StreamFn = (_model, _context, options) => { - const payload: Record = { - reasoning: { effort: "minimal", summary: "auto" }, - tools: [{ type: "function", name: "web_search" }], - }; - options?.onPayload?.(payload, _model); - payloads.push(structuredClone(payload)); - return createAssistantMessageEventStream(); - }; - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "minimal"); - void wrapped( - { - api: "openai-responses", - provider: "openai", - id: "gpt-5", - baseUrl: "http://127.0.0.1:19191/v1", - } as Model<"openai-responses">, - { messages: [] }, - {}, - ); - - expect(payloads[0]?.reasoning).toEqual({ effort: "low", summary: "auto" }); - }); - - it.each([ - { - api: "openai-responses", - provider: "openai", - id: "gpt-5.5", - }, - { - api: "openai-codex-responses", - provider: "openai-codex", - id: "gpt-5.5", - }, - ] as const)("preserves xhigh for $provider/$id", (model) => { - const { baseStreamFn, payloads } = createPayloadCapture({ - initialReasoning: { effort: "high" }, - }); - const wrapped = createOpenAIThinkingLevelWrapper(baseStreamFn, "xhigh"); - void wrapped(model as Model, { messages: [] }, {}); - - expect(payloads[0]?.reasoning).toEqual({ effort: "xhigh" }); - }); -}); - -describe("createOpenAIAttributionHeadersWrapper", () => { - it("routes native Codex traffic through the OpenClaw transport so attribution survives PI defaults", () => { - let codexCalls = 0; - let capturedHeaders: Record | undefined; - const codexTransport: StreamFn = (_model, _context, options) => { - codexCalls += 1; - capturedHeaders = options?.headers; - return createAssistantMessageEventStream(); - }; - const wrapped = createOpenAIAttributionHeadersWrapper(undefined, { - codexNativeTransportStreamFn: codexTransport, - }); - - void wrapped( - { - ...codexModel, - baseUrl: "https://chatgpt.com/backend-api", - } as Model<"openai-codex-responses">, - { messages: [] }, - { - headers: { - originator: "pi", - "User-Agent": "pi", - }, - }, - ); - - expect(codexCalls).toBe(1); - expect(capturedHeaders?.originator).toBe("openclaw"); - expect(capturedHeaders?.["User-Agent"]).toMatch(/^openclaw\//); - }); - - it("keeps existing wrapped Codex streams so runtime OAuth injection is preserved", () => { - let upstreamCalls = 0; - let codexCalls = 0; - let capturedOptions: - | { - apiKey?: string; - headers?: Record; - } - | undefined; - const upstream: StreamFn = (_model, _context, options) => { - upstreamCalls += 1; - capturedOptions = options; - return createAssistantMessageEventStream(); - }; - const codexTransport: StreamFn = () => { - codexCalls += 1; - return createAssistantMessageEventStream(); - }; - const wrapped = createOpenAIAttributionHeadersWrapper(upstream, { - codexNativeTransportStreamFn: codexTransport, - }); - - void wrapped( - { - ...codexModel, - baseUrl: "https://chatgpt.com/backend-api", - } as Model<"openai-codex-responses">, - { messages: [] }, - { - apiKey: "oauth-bearer-token", - headers: { - originator: "pi", - "User-Agent": "pi", - }, - }, - ); - - expect(upstreamCalls).toBe(1); - expect(codexCalls).toBe(0); - expect(capturedOptions?.apiKey).toBe("oauth-bearer-token"); - expect(capturedOptions?.headers?.originator).toBe("openclaw"); - expect(capturedOptions?.headers?.["User-Agent"]).toMatch(/^openclaw\//); - }); -}); diff --git a/src/agents/pi-embedded-runner/openai-stream-wrappers.ts b/src/agents/pi-embedded-runner/openai-stream-wrappers.ts deleted file mode 100644 index 51a3ee6abce..00000000000 --- a/src/agents/pi-embedded-runner/openai-stream-wrappers.ts +++ /dev/null @@ -1,704 +0,0 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { SimpleStreamOptions } from "@earendil-works/pi-ai"; -import { streamSimple } from "@earendil-works/pi-ai"; -import type { ThinkLevel } from "../../auto-reply/thinking.js"; -import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { isRecord } from "../../shared/record-coerce.js"; -import { normalizeOptionalLowercaseString, readStringValue } from "../../shared/string-coerce.js"; -import { - patchCodexNativeWebSearchPayload, - resolveCodexNativeSearchActivation, -} from "../codex-native-web-search-core.js"; -import { emitModelTransportDebug } from "../model-transport-debug.js"; -import { - flattenCompletionMessagesToStringContent, - stripCompletionMessagesToRoleContent, -} from "../openai-completions-string-content.js"; -import { resolveOpenAIReasoningEffortForModel } from "../openai-reasoning-effort.js"; -import { - applyOpenAIResponsesPayloadPolicy, - resolveOpenAIResponsesPayloadPolicy, -} from "../openai-responses-payload-policy.js"; -import { resolveOpenAITextVerbosity, type OpenAITextVerbosity } from "../openai-text-verbosity.js"; -import { createOpenAIResponsesTransportStreamFn } from "../openai-transport-stream.js"; -import { resolveProviderRequestPolicyConfig } from "../provider-request-config.js"; -import { log } from "./logger.js"; -import { mapThinkingLevelToReasoningEffort } from "./reasoning-effort-utils.js"; -import { streamWithPayloadPatch } from "./stream-payload-utils.js"; - -type OpenAIServiceTier = "auto" | "default" | "flex" | "priority"; -type OpenClawSimpleStreamOptions = SimpleStreamOptions & { - openclawCodeModeToolSurface?: boolean; -}; -export { resolveOpenAITextVerbosity }; - -function resolveOpenAITextVerbosityForModel( - model: { api?: unknown; id?: unknown; provider?: unknown }, - verbosity: OpenAITextVerbosity, -): OpenAITextVerbosity { - const api = normalizeOptionalLowercaseString(model.api); - const provider = normalizeOptionalLowercaseString(model.provider); - const id = normalizeOptionalLowercaseString(model.id); - if (api === "openai-responses" && provider === "openai" && id === "chat-latest") { - return "medium"; - } - return verbosity; -} - -function resolveOpenAIRequestCapabilities(model: { - api?: unknown; - provider?: unknown; - baseUrl?: unknown; - compat?: unknown; -}) { - const compat = - model.compat && typeof model.compat === "object" - ? (model.compat as { supportsStore?: boolean }) - : undefined; - return resolveProviderRequestPolicyConfig({ - provider: readStringValue(model.provider), - api: readStringValue(model.api), - baseUrl: readStringValue(model.baseUrl), - compat, - capability: "llm", - transport: "stream", - }).capabilities; -} - -function shouldApplyOpenAIAttributionHeaders(model: { - api?: unknown; - provider?: unknown; - baseUrl?: unknown; -}): "openai" | "openai-codex" | undefined { - const attributionProvider = resolveOpenAIRequestCapabilities(model).attributionProvider; - return attributionProvider === "openai" || attributionProvider === "openai-codex" - ? attributionProvider - : undefined; -} - -function shouldApplyOpenAIServiceTier(model: { - api?: unknown; - provider?: unknown; - baseUrl?: unknown; -}): boolean { - return resolveOpenAIResponsesPayloadPolicy(model, { storeMode: "disable" }).allowsServiceTier; -} - -function isCodeModeEnabled(config?: OpenClawConfig): boolean { - const tools = config?.tools; - if (!tools || typeof tools !== "object") { - return false; - } - const codeMode = (tools as { codeMode?: unknown }).codeMode; - if (codeMode === true) { - return true; - } - return Boolean( - codeMode && - typeof codeMode === "object" && - (codeMode as { enabled?: unknown }).enabled === true, - ); -} - -function readPayloadToolName(tool: unknown): string | undefined { - if (!tool || typeof tool !== "object") { - return undefined; - } - const record = tool as { name?: unknown; function?: { name?: unknown } }; - if (typeof record.name === "string") { - return record.name; - } - return typeof record.function?.name === "string" ? record.function.name : undefined; -} - -function isCodeModePayloadToolName(name: string | undefined): boolean { - return name === "exec" || name === "wait"; -} - -function filterCodeModeToolDeclarations(declarations: unknown): unknown[] | undefined { - if (!Array.isArray(declarations)) { - return undefined; - } - return declarations.filter((declaration) => - isCodeModePayloadToolName(readPayloadToolName(declaration)), - ); -} - -function filterCodeModeGroupedToolDeclarations(tool: unknown): Record | undefined { - if (!tool || typeof tool !== "object" || Array.isArray(tool)) { - return undefined; - } - const record = tool as Record; - const filteredGroups: Record = {}; - for (const key of ["functionDeclarations", "function_declarations"] as const) { - const filtered = filterCodeModeToolDeclarations(record[key]); - if (filtered === undefined) { - continue; - } - if (filtered.length > 0) { - filteredGroups[key] = filtered; - } - } - return Object.keys(filteredGroups).length > 0 ? filteredGroups : undefined; -} - -function filterCodeModePayloadTools(payload: unknown): void { - if (!payload || typeof payload !== "object") { - return; - } - const record = payload as { tools?: unknown }; - if (!Array.isArray(record.tools)) { - return; - } - record.tools = record.tools.flatMap((tool) => { - const name = readPayloadToolName(tool); - if (isCodeModePayloadToolName(name)) { - return [tool]; - } - const grouped = filterCodeModeGroupedToolDeclarations(tool); - return grouped ? [grouped] : []; - }); -} - -function hasCodeModeVisibleTools(context: { tools?: unknown }): boolean { - if (!Array.isArray(context.tools)) { - return false; - } - const names = new Set(context.tools.map(readPayloadToolName).filter(Boolean)); - return names.has("exec") && names.has("wait"); -} - -function shouldApplyOpenAIReasoningCompatibility(model: { - api?: unknown; - provider?: unknown; - baseUrl?: unknown; -}): boolean { - const api = readStringValue(model.api); - const provider = readStringValue(model.provider); - if (!api || !provider) { - return false; - } - return resolveOpenAIRequestCapabilities(model).supportsOpenAIReasoningCompatPayload; -} - -function shouldFlattenOpenAICompletionMessages(model: { - api?: unknown; - compat?: unknown; -}): boolean { - const compat = - model.compat && typeof model.compat === "object" - ? (model.compat as { requiresStringContent?: unknown }) - : undefined; - return model.api === "openai-completions" && compat?.requiresStringContent === true; -} - -function shouldStripOpenAICompletionTools(model: { api?: unknown; compat?: unknown }): boolean { - const compat = - model.compat && typeof model.compat === "object" - ? (model.compat as { supportsTools?: unknown }) - : undefined; - return model.api === "openai-completions" && compat?.supportsTools === false; -} - -function shouldStripOpenAICompletionMessageKeys(model: { - api?: unknown; - compat?: unknown; -}): boolean { - const compat = - model.compat && typeof model.compat === "object" - ? (model.compat as { strictMessageKeys?: unknown }) - : undefined; - return model.api === "openai-completions" && compat?.strictMessageKeys === true; -} - -function hasResponsesWebSearchTool(tools: unknown): boolean { - if (!Array.isArray(tools)) { - return false; - } - return tools.some((tool) => { - if (!isRecord(tool)) { - return false; - } - if (tool.type === "web_search") { - return true; - } - if (tool.type === "function" && tool.name === "web_search") { - return true; - } - const fn = tool.function; - return isRecord(fn) && fn.name === "web_search"; - }); -} - -function resolveOpenAIThinkingPayloadEffort(params: { - model: { provider?: unknown; id?: unknown; baseUrl?: unknown; api?: unknown; compat?: unknown }; - payloadObj: Record; - thinkingLevel: ThinkLevel; -}) { - const mapped = mapThinkingLevelToReasoningEffort(params.thinkingLevel); - if (mapped !== "minimal" || !hasResponsesWebSearchTool(params.payloadObj.tools)) { - return mapped; - } - return ( - resolveOpenAIReasoningEffortForModel({ - model: params.model, - effort: "low", - }) ?? mapped - ); -} - -function raiseMinimalReasoningForResponsesWebSearchPayload(params: { - model: { provider?: unknown; id?: unknown; baseUrl?: unknown; api?: unknown; compat?: unknown }; - payloadObj: Record; -}): void { - const reasoning = params.payloadObj.reasoning; - if (!isRecord(reasoning) || reasoning.effort !== "minimal") { - return; - } - if (!hasResponsesWebSearchTool(params.payloadObj.tools)) { - return; - } - const nextEffort = resolveOpenAIReasoningEffortForModel({ - model: params.model, - effort: "low", - }); - if (nextEffort && nextEffort !== "minimal" && nextEffort !== "none") { - reasoning.effort = nextEffort; - } -} - -function normalizeOpenAIServiceTier(value: unknown): OpenAIServiceTier | undefined { - if (typeof value !== "string") { - return undefined; - } - const normalized = normalizeOptionalLowercaseString(value); - if ( - normalized === "auto" || - normalized === "default" || - normalized === "flex" || - normalized === "priority" - ) { - return normalized; - } - return undefined; -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function resolveOpenAIServiceTier( - extraParams: Record | undefined, -): OpenAIServiceTier | undefined { - const raw = extraParams?.serviceTier ?? extraParams?.service_tier; - const normalized = normalizeOpenAIServiceTier(raw); - if (raw !== undefined && normalized === undefined) { - const rawSummary = typeof raw === "string" ? raw : typeof raw; - log.warn(`ignoring invalid OpenAI service tier param: ${rawSummary}`); - } - return normalized; -} - -function normalizeOpenAIFastMode(value: unknown): boolean | undefined { - if (typeof value === "boolean") { - return value; - } - const normalized = normalizeOptionalLowercaseString(value); - if (!normalized) { - return undefined; - } - if ( - normalized === "on" || - normalized === "true" || - normalized === "yes" || - normalized === "1" || - normalized === "fast" - ) { - return true; - } - if ( - normalized === "off" || - normalized === "false" || - normalized === "no" || - normalized === "0" || - normalized === "normal" - ) { - return false; - } - return undefined; -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function resolveOpenAIFastMode( - extraParams: Record | undefined, -): boolean | undefined { - const raw = extraParams?.fastMode ?? extraParams?.fast_mode; - const normalized = normalizeOpenAIFastMode(raw); - if (raw !== undefined && normalized === undefined) { - const rawSummary = typeof raw === "string" ? raw : typeof raw; - log.warn(`ignoring invalid OpenAI fast mode param: ${rawSummary}`); - } - return normalized; -} - -function applyOpenAIFastModePayloadOverrides(params: { - payloadObj: Record; - model: { provider?: unknown; id?: unknown; baseUrl?: unknown; api?: unknown }; -}): void { - if (params.payloadObj.service_tier === undefined && shouldApplyOpenAIServiceTier(params.model)) { - params.payloadObj.service_tier = "priority"; - } -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenAIResponsesContextManagementWrapper( - baseStreamFn: StreamFn | undefined, - extraParams: Record | undefined, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - const policy = resolveOpenAIResponsesPayloadPolicy(model, { - extraParams, - enablePromptCacheStripping: true, - enableServerCompaction: true, - storeMode: "provider-policy", - }); - if ( - policy.explicitStore === undefined && - !policy.useServerCompaction && - !policy.shouldStripStore && - !policy.shouldStripPromptCache && - !policy.shouldStripDisabledReasoningPayload - ) { - return underlying(model, context, options); - } - - const originalOnPayload = options?.onPayload; - return underlying(model, context, { - ...options, - onPayload: (payload) => { - if (payload && typeof payload === "object") { - applyOpenAIResponsesPayloadPolicy(payload as Record, policy); - } - return originalOnPayload?.(payload, model); - }, - }); - }; -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenAIReasoningCompatibilityWrapper( - baseStreamFn: StreamFn | undefined, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - if (!shouldApplyOpenAIReasoningCompatibility(model)) { - return underlying(model, context, options); - } - return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { - applyOpenAIResponsesPayloadPolicy( - payloadObj, - resolveOpenAIResponsesPayloadPolicy(model, { storeMode: "preserve" }), - ); - }); - }; -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenAIStringContentWrapper(baseStreamFn: StreamFn | undefined): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - if (!shouldFlattenOpenAICompletionMessages(model)) { - return underlying(model, context, options); - } - return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { - if (!Array.isArray(payloadObj.messages)) { - return; - } - payloadObj.messages = flattenCompletionMessagesToStringContent(payloadObj.messages); - }); - }; -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenAICompletionsStrictMessageKeysWrapper( - baseStreamFn: StreamFn | undefined, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - if (!shouldStripOpenAICompletionMessageKeys(model)) { - return underlying(model, context, options); - } - return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { - if (!Array.isArray(payloadObj.messages)) { - return; - } - payloadObj.messages = stripCompletionMessagesToRoleContent(payloadObj.messages); - }); - }; -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenAICompletionsToolsCompatWrapper( - baseStreamFn: StreamFn | undefined, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - if (!shouldStripOpenAICompletionTools(model)) { - return underlying(model, context, options); - } - return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { - delete payloadObj.tools; - delete payloadObj.tool_choice; - delete payloadObj.parallel_tool_calls; - }); - }; -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenAIThinkingLevelWrapper( - baseStreamFn: StreamFn | undefined, - thinkingLevel?: ThinkLevel, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - if (!thinkingLevel) { - return underlying; - } - return (model, context, options) => { - if (!shouldApplyOpenAIReasoningCompatibility(model)) { - if (thinkingLevel === "off") { - return underlying(model, context, options); - } - return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { - raiseMinimalReasoningForResponsesWebSearchPayload({ model, payloadObj }); - }); - } - return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { - const existingReasoning = payloadObj.reasoning; - if (thinkingLevel === "off") { - if (existingReasoning !== undefined) { - delete payloadObj.reasoning; - } - return; - } - - const reasoningEffort = resolveOpenAIThinkingPayloadEffort({ - model, - payloadObj, - thinkingLevel, - }); - if (existingReasoning === "none") { - payloadObj.reasoning = { effort: reasoningEffort }; - return; - } - if ( - existingReasoning && - typeof existingReasoning === "object" && - !Array.isArray(existingReasoning) - ) { - (existingReasoning as Record).effort = reasoningEffort; - raiseMinimalReasoningForResponsesWebSearchPayload({ model, payloadObj }); - } - }); - }; -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenAIFastModeWrapper(baseStreamFn: StreamFn | undefined): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - if ( - (model.api !== "openai-responses" && - model.api !== "openai-codex-responses" && - model.api !== "azure-openai-responses") || - (model.provider !== "openai" && model.provider !== "openai-codex") - ) { - return underlying(model, context, options); - } - const originalOnPayload = options?.onPayload; - return underlying(model, context, { - ...options, - onPayload: (payload) => { - if (payload && typeof payload === "object") { - applyOpenAIFastModePayloadOverrides({ - payloadObj: payload as Record, - model, - }); - } - return originalOnPayload?.(payload, model); - }, - }); - }; -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenAIServiceTierWrapper( - baseStreamFn: StreamFn | undefined, - serviceTier: OpenAIServiceTier, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - if (!shouldApplyOpenAIServiceTier(model)) { - return underlying(model, context, options); - } - return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { - if (payloadObj.service_tier === undefined) { - payloadObj.service_tier = serviceTier; - } - }); - }; -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenAITextVerbosityWrapper( - baseStreamFn: StreamFn | undefined, - verbosity: OpenAITextVerbosity, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - if (model.api !== "openai-responses" && model.api !== "openai-codex-responses") { - return underlying(model, context, options); - } - const resolvedVerbosity = resolveOpenAITextVerbosityForModel(model, verbosity); - const shouldOverrideExistingVerbosity = - model.api === "openai-codex-responses" || resolvedVerbosity !== verbosity; - const originalOnPayload = options?.onPayload; - return underlying(model, context, { - ...options, - onPayload: (payload) => { - if (payload && typeof payload === "object") { - const payloadObj = payload as Record; - const existingText = - payloadObj.text && typeof payloadObj.text === "object" - ? (payloadObj.text as Record) - : {}; - if (shouldOverrideExistingVerbosity || existingText.verbosity === undefined) { - payloadObj.text = { ...existingText, verbosity: resolvedVerbosity }; - } - } - return originalOnPayload?.(payload, model); - }, - }); - }; -} -/** @deprecated OpenAI Codex provider-owned stream helper; do not use from third-party plugins. */ -export function createCodexNativeWebSearchWrapper( - baseStreamFn: StreamFn | undefined, - params: { config?: OpenClawConfig; agentDir?: string; codeModeToolSurfaceEnabled?: boolean }, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - if ( - (params.codeModeToolSurfaceEnabled === true || isCodeModeEnabled(params.config)) && - hasCodeModeVisibleTools(context) - ) { - emitModelTransportDebug( - log, - `skipping Codex native web search because code mode owns the model tool surface for ${ - model.provider ?? "unknown" - }/${model.id ?? "unknown"}`, - ); - const originalOnPayload = options?.onPayload; - const codeModeOptions: OpenClawSimpleStreamOptions = { - ...options, - openclawCodeModeToolSurface: true, - onPayload: (payload) => { - filterCodeModePayloadTools(payload); - const nextPayload = originalOnPayload?.(payload, model); - if (nextPayload !== undefined) { - filterCodeModePayloadTools(nextPayload); - return nextPayload; - } - filterCodeModePayloadTools(payload); - return undefined; - }, - }; - return underlying(model, context, codeModeOptions); - } - - const activation = resolveCodexNativeSearchActivation({ - config: params.config, - modelProvider: readStringValue(model.provider), - modelApi: readStringValue(model.api), - agentDir: params.agentDir, - }); - - if (activation.state !== "native_active") { - if (activation.codexNativeEnabled) { - log.debug( - `skipping Codex native web search (${activation.inactiveReason ?? "inactive"}) for ${ - model.provider ?? "unknown" - }/${model.id ?? "unknown"}`, - ); - } - return underlying(model, context, options); - } - - log.debug( - `activating Codex native web search (${activation.codexMode}) for ${ - model.provider ?? "unknown" - }/${model.id ?? "unknown"}`, - ); - - const originalOnPayload = options?.onPayload; - return underlying(model, context, { - ...options, - onPayload: (payload) => { - const result = patchCodexNativeWebSearchPayload({ - payload, - config: params.config, - }); - if (result.status === "payload_not_object") { - log.debug( - "Skipping Codex native web search injection because provider payload is not an object", - ); - } else if (result.status === "native_tool_already_present") { - log.debug("Codex native web search tool already present in provider payload"); - } else if (result.status === "injected") { - log.debug("Injected Codex native web search tool into provider payload"); - } - return originalOnPayload?.(payload, model); - }, - }); - }; -} -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenAIDefaultTransportWrapper(baseStreamFn: StreamFn | undefined): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - const mergedOptions = { - ...options, - transport: options?.transport ?? "auto", - } as SimpleStreamOptions; - return underlying(model, context, mergedOptions); - }; -} - -/** @deprecated OpenAI provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenAIAttributionHeadersWrapper( - baseStreamFn: StreamFn | undefined, - opts?: { codexNativeTransportStreamFn?: StreamFn }, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - const attributionProvider = shouldApplyOpenAIAttributionHeaders(model); - if (!attributionProvider) { - return underlying(model, context, options); - } - const shouldCreateCodexTransport = - attributionProvider === "openai-codex" && - (baseStreamFn === undefined || baseStreamFn === streamSimple); - const streamFn = shouldCreateCodexTransport - ? (opts?.codexNativeTransportStreamFn ?? createOpenAIResponsesTransportStreamFn()) - : underlying; - return streamFn(model, context, { - ...options, - headers: resolveProviderRequestPolicyConfig({ - provider: attributionProvider, - api: readStringValue(model.api), - baseUrl: readStringValue(model.baseUrl), - capability: "llm", - transport: "stream", - callerHeaders: options?.headers, - precedence: "defaults-win", - }).headers, - }); - }; -} diff --git a/src/agents/pi-embedded-runner/proxy-stream-wrappers.test.ts b/src/agents/pi-embedded-runner/proxy-stream-wrappers.test.ts deleted file mode 100644 index 35a6a3546f3..00000000000 --- a/src/agents/pi-embedded-runner/proxy-stream-wrappers.test.ts +++ /dev/null @@ -1,207 +0,0 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { Context, Model } from "@earendil-works/pi-ai"; -import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; -import { describe, expect, it } from "vitest"; -import { - createOpenRouterSystemCacheWrapper, - createOpenRouterWrapper, -} from "./proxy-stream-wrappers.js"; - -function runSystemCacheWrapper(model: Partial>) { - const payload = { - messages: [{ role: "system", content: "system prompt" }], - }; - const baseStreamFn: StreamFn = (resolvedModel, _context, options) => { - options?.onPayload?.(payload, resolvedModel); - return createAssistantMessageEventStream(); - }; - - const wrapped = createOpenRouterSystemCacheWrapper(baseStreamFn); - void wrapped( - { - api: "openai-completions", - provider: "openrouter", - id: "anthropic/claude-sonnet-4.6", - ...model, - } as Model<"openai-completions">, - { messages: [] }, - {}, - ); - - return payload; -} - -describe("proxy stream wrappers", () => { - it("adds OpenRouter attribution headers to stream options", () => { - const calls: Array<{ headers?: Record }> = []; - const baseStreamFn: StreamFn = (_model, _context, options) => { - calls.push({ - headers: options?.headers, - }); - return createAssistantMessageEventStream(); - }; - - const wrapped = createOpenRouterWrapper(baseStreamFn); - const model = { - api: "openai-completions", - provider: "openrouter", - id: "openrouter/auto", - } as Model<"openai-completions">; - const context: Context = { messages: [] }; - - void wrapped(model, context, { headers: { "X-Custom": "1" } }); - - expect(calls).toEqual([ - { - headers: { - "HTTP-Referer": "https://openclaw.ai", - "X-OpenRouter-Title": "OpenClaw", - "X-OpenRouter-Categories": - "cli-agent,cloud-agent,programming-app,creative-writing,writing-assistant,general-chat,personal-agent", - "X-Custom": "1", - }, - }, - ]); - }); - - it("adds opt-in OpenRouter response caching headers", () => { - const calls: Array<{ headers?: Record }> = []; - const baseStreamFn: StreamFn = (_model, _context, options) => { - calls.push({ headers: options?.headers }); - return createAssistantMessageEventStream(); - }; - - const wrapped = createOpenRouterWrapper(baseStreamFn, undefined, { - responseCache: true, - responseCacheTtlSeconds: 900, - }); - - void wrapped( - { - api: "openai-completions", - provider: "openrouter", - id: "openrouter/auto", - baseUrl: "https://openrouter.ai/api/v1", - } as Model<"openai-completions">, - { messages: [] }, - {}, - ); - - expect(calls[0]?.headers?.["HTTP-Referer"]).toBe("https://openclaw.ai"); - expect(calls[0]?.headers?.["X-OpenRouter-Cache"]).toBe("true"); - expect(calls[0]?.headers?.["X-OpenRouter-Cache-TTL"]).toBe("900"); - }); - - it("sends OpenRouter response cache disables for preset opt-outs", () => { - const calls: Array<{ headers?: Record }> = []; - const baseStreamFn: StreamFn = (_model, _context, options) => { - calls.push({ headers: options?.headers }); - return createAssistantMessageEventStream(); - }; - - const wrapped = createOpenRouterWrapper(baseStreamFn, undefined, { - response_cache: false, - response_cache_ttl_seconds: 600, - }); - - void wrapped( - { - api: "openai-completions", - provider: "openrouter", - id: "openrouter/@preset/cached-tests", - } as Model<"openai-completions">, - { messages: [] }, - {}, - ); - - expect(calls[0]?.headers?.["X-OpenRouter-Cache"]).toBe("false"); - expect(calls[0]?.headers).not.toHaveProperty("X-OpenRouter-Cache-TTL"); - }); - - it("supports OpenRouter response cache refresh and TTL clamping", () => { - const calls: Array<{ headers?: Record }> = []; - const baseStreamFn: StreamFn = (_model, _context, options) => { - calls.push({ headers: options?.headers }); - return createAssistantMessageEventStream(); - }; - - const wrapped = createOpenRouterWrapper(baseStreamFn, undefined, { - response_cache_clear: "true", - response_cache_ttl: 999999, - }); - - void wrapped( - { - api: "openai-completions", - provider: "openrouter", - id: "openrouter/auto", - } as Model<"openai-completions">, - { messages: [] }, - {}, - ); - - expect(calls[0]?.headers?.["X-OpenRouter-Cache"]).toBe("true"); - expect(calls[0]?.headers?.["X-OpenRouter-Cache-Clear"]).toBe("true"); - expect(calls[0]?.headers?.["X-OpenRouter-Cache-TTL"]).toBe("86400"); - }); - - it("does not add OpenRouter response caching headers to custom proxy routes", () => { - const calls: Array<{ headers?: Record }> = []; - const baseStreamFn: StreamFn = (_model, _context, options) => { - calls.push({ headers: options?.headers }); - return createAssistantMessageEventStream(); - }; - - const wrapped = createOpenRouterWrapper(baseStreamFn, undefined, { - responseCache: true, - }); - - void wrapped( - { - api: "openai-completions", - provider: "openrouter", - id: "openrouter/auto", - baseUrl: "https://proxy.example.com/v1", - } as Model<"openai-completions">, - { messages: [] }, - {}, - ); - - expect(calls[0]?.headers).toBeUndefined(); - }); - - it("injects cache_control markers for declared OpenRouter Anthropic models on the default route", () => { - const payload = runSystemCacheWrapper({}); - - expect(payload.messages[0]?.content).toEqual([ - { type: "text", text: "system prompt", cache_control: { type: "ephemeral" } }, - ]); - }); - - it("does not inject cache_control markers for declared OpenRouter providers on custom proxy URLs", () => { - const payload = runSystemCacheWrapper({ - baseUrl: "https://proxy.example.com/v1", - }); - - expect(payload.messages[0]?.content).toBe("system prompt"); - }); - - it("does not inject Anthropic cache_control markers for automatic OpenRouter DeepSeek cache models", () => { - const payload = runSystemCacheWrapper({ - id: "deepseek/deepseek-v3.2", - }); - - expect(payload.messages[0]?.content).toBe("system prompt"); - }); - - it("injects cache_control markers for native OpenRouter hosts behind custom provider ids", () => { - const payload = runSystemCacheWrapper({ - provider: "custom-openrouter", - baseUrl: "https://openrouter.ai/api/v1", - }); - - expect(payload.messages[0]?.content).toEqual([ - { type: "text", text: "system prompt", cache_control: { type: "ephemeral" } }, - ]); - }); -}); diff --git a/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts b/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts deleted file mode 100644 index cb8d7306802..00000000000 --- a/src/agents/pi-embedded-runner/proxy-stream-wrappers.ts +++ /dev/null @@ -1,256 +0,0 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { streamSimple } from "@earendil-works/pi-ai"; -import type { ThinkLevel } from "../../auto-reply/thinking.js"; -import { normalizeOptionalLowercaseString, readStringValue } from "../../shared/string-coerce.js"; -import { resolveProviderRequestPolicy } from "../provider-attribution.js"; -import { resolveProviderRequestPolicyConfig } from "../provider-request-config.js"; -import { applyAnthropicEphemeralCacheControlMarkers } from "./anthropic-cache-control-payload.js"; -import { isAnthropicModelRef } from "./anthropic-family-cache-semantics.js"; -import { mapThinkingLevelToReasoningEffort } from "./reasoning-effort-utils.js"; -import { streamWithPayloadPatch } from "./stream-payload-utils.js"; -const KILOCODE_FEATURE_HEADER = "X-KILOCODE-FEATURE"; -const KILOCODE_FEATURE_DEFAULT = "openclaw"; -const KILOCODE_FEATURE_ENV_VAR = "KILOCODE_FEATURE"; - -function resolveKilocodeAppHeaders(): Record { - const feature = process.env[KILOCODE_FEATURE_ENV_VAR]?.trim() || KILOCODE_FEATURE_DEFAULT; - return { [KILOCODE_FEATURE_HEADER]: feature }; -} - -function readExtraParam( - extraParams: Record | undefined, - keys: readonly string[], -): unknown { - if (!extraParams) { - return undefined; - } - for (const key of keys) { - if (Object.hasOwn(extraParams, key)) { - return extraParams[key]; - } - } - return undefined; -} - -function resolveBooleanParam(value: unknown): boolean | undefined { - if (typeof value === "boolean") { - return value; - } - if (typeof value !== "string") { - return undefined; - } - const normalized = normalizeOptionalLowercaseString(value); - if (!normalized) { - return undefined; - } - if (["1", "true", "yes", "on", "enable", "enabled"].includes(normalized)) { - return true; - } - if (["0", "false", "no", "off", "disable", "disabled"].includes(normalized)) { - return false; - } - return undefined; -} - -function resolveOpenRouterResponseCacheTtlSeconds(value: unknown): string | undefined { - const parsed = - typeof value === "number" - ? value - : typeof value === "string" - ? Number.parseFloat(value.trim()) - : Number.NaN; - if (!Number.isFinite(parsed)) { - return undefined; - } - return String(Math.max(1, Math.min(86400, Math.trunc(parsed)))); -} - -function shouldApplyOpenRouterResponseCacheHeaders(model: Parameters[0]): boolean { - const provider = readStringValue(model.provider); - const endpointClass = resolveProviderRequestPolicy({ - provider, - api: readStringValue(model.api), - baseUrl: readStringValue(model.baseUrl), - capability: "llm", - transport: "stream", - }).endpointClass; - return ( - endpointClass === "openrouter" || - (endpointClass === "default" && normalizeOptionalLowercaseString(provider) === "openrouter") - ); -} - -function resolveOpenRouterResponseCacheHeaders( - model: Parameters[0], - extraParams: Record | undefined, -): Record | undefined { - if (!shouldApplyOpenRouterResponseCacheHeaders(model)) { - return undefined; - } - const configuredCache = resolveBooleanParam( - readExtraParam(extraParams, ["responseCache", "response_cache"]), - ); - const clearCache = resolveBooleanParam( - readExtraParam(extraParams, ["responseCacheClear", "response_cache_clear"]), - ); - const cacheEnabled = configuredCache ?? (clearCache ? true : undefined); - if (cacheEnabled === undefined) { - return undefined; - } - - const headers: Record = { - "X-OpenRouter-Cache": cacheEnabled ? "true" : "false", - }; - if (!cacheEnabled) { - return headers; - } - - const ttl = resolveOpenRouterResponseCacheTtlSeconds( - readExtraParam(extraParams, [ - "responseCacheTtlSeconds", - "response_cache_ttl_seconds", - "responseCacheTtl", - "response_cache_ttl", - ]), - ); - if (ttl) { - headers["X-OpenRouter-Cache-TTL"] = ttl; - } - if (clearCache) { - headers["X-OpenRouter-Cache-Clear"] = "true"; - } - return headers; -} - -function normalizeProxyReasoningPayload(payload: unknown, thinkingLevel?: ThinkLevel): void { - if (!payload || typeof payload !== "object") { - return; - } - - const payloadObj = payload as Record; - delete payloadObj.reasoning_effort; - if (!thinkingLevel || thinkingLevel === "off") { - return; - } - - const existingReasoning = payloadObj.reasoning; - if ( - existingReasoning && - typeof existingReasoning === "object" && - !Array.isArray(existingReasoning) - ) { - const reasoningObj = existingReasoning as Record; - if (!("max_tokens" in reasoningObj) && !("effort" in reasoningObj)) { - reasoningObj.effort = mapThinkingLevelToReasoningEffort(thinkingLevel); - } - } else if (!existingReasoning) { - payloadObj.reasoning = { - effort: mapThinkingLevelToReasoningEffort(thinkingLevel), - }; - } -} - -/** @deprecated OpenRouter provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenRouterSystemCacheWrapper(baseStreamFn: StreamFn | undefined): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - const provider = readStringValue(model.provider); - const modelId = readStringValue(model.id); - // Keep OpenRouter-specific cache markers on verified OpenRouter routes - // (or the provider's default route), but not on arbitrary OpenAI proxies. - const endpointClass = resolveProviderRequestPolicy({ - provider, - api: readStringValue(model.api), - baseUrl: readStringValue(model.baseUrl), - capability: "llm", - transport: "stream", - }).endpointClass; - if ( - !modelId || - !isAnthropicModelRef(modelId) || - !( - endpointClass === "openrouter" || - (endpointClass === "default" && normalizeOptionalLowercaseString(provider) === "openrouter") - ) - ) { - return underlying(model, context, options); - } - - return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { - applyAnthropicEphemeralCacheControlMarkers(payloadObj); - }); - }; -} - -/** @deprecated OpenRouter provider-owned stream helper; do not use from third-party plugins. */ -export function createOpenRouterWrapper( - baseStreamFn: StreamFn | undefined, - thinkingLevel?: ThinkLevel, - extraParams?: Record, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - const providerHeaders = resolveOpenRouterResponseCacheHeaders(model, extraParams); - const headers = resolveProviderRequestPolicyConfig({ - provider: readStringValue(model.provider) ?? "openrouter", - api: readStringValue(model.api), - baseUrl: readStringValue(model.baseUrl), - capability: "llm", - transport: "stream", - callerHeaders: options?.headers, - providerHeaders, - precedence: "caller-wins", - }).headers; - return streamWithPayloadPatch( - underlying, - model, - context, - { - ...options, - headers, - }, - (payload) => { - normalizeProxyReasoningPayload(payload, thinkingLevel); - }, - ); - }; -} - -/** @deprecated Proxy provider-owned stream helper; do not use from third-party plugins. */ -export function isProxyReasoningUnsupported(modelId: string): boolean { - const trimmed = normalizeOptionalLowercaseString(modelId); - const slashIndex = trimmed?.indexOf("/") ?? -1; - return slashIndex > 0 && trimmed?.slice(0, slashIndex) === "x-ai"; -} - -/** @deprecated Kilocode provider-owned stream helper; do not use from third-party plugins. */ -export function createKilocodeWrapper( - baseStreamFn: StreamFn | undefined, - thinkingLevel?: ThinkLevel, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - const headers = resolveProviderRequestPolicyConfig({ - provider: readStringValue(model.provider) ?? "kilocode", - api: readStringValue(model.api), - baseUrl: readStringValue(model.baseUrl), - capability: "llm", - transport: "stream", - callerHeaders: options?.headers, - providerHeaders: resolveKilocodeAppHeaders(), - precedence: "defaults-win", - }).headers; - return streamWithPayloadPatch( - underlying, - model, - context, - { - ...options, - headers, - }, - (payload) => { - normalizeProxyReasoningPayload(payload, thinkingLevel); - }, - ); - }; -} diff --git a/src/agents/pi-embedded-runner/reasoning-effort-utils.test.ts b/src/agents/pi-embedded-runner/reasoning-effort-utils.test.ts deleted file mode 100644 index 5cf8c3101e4..00000000000 --- a/src/agents/pi-embedded-runner/reasoning-effort-utils.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { mapThinkingLevelToReasoningEffort } from "./reasoning-effort-utils.js"; - -describe("mapThinkingLevelToReasoningEffort", () => { - it('maps "off" to "none"', () => { - expect(mapThinkingLevelToReasoningEffort("off")).toBe("none"); - }); - - it('maps "adaptive" to "medium"', () => { - expect(mapThinkingLevelToReasoningEffort("adaptive")).toBe("medium"); - }); - - it('maps "max" to "xhigh"', () => { - expect(mapThinkingLevelToReasoningEffort("max")).toBe("xhigh"); - }); - - it.each(["minimal", "low", "medium", "high", "xhigh"] as const)( - "passes through %s unchanged", - (level) => { - expect(mapThinkingLevelToReasoningEffort(level)).toBe(level); - }, - ); -}); diff --git a/src/agents/pi-embedded-runner/reasoning-effort-utils.ts b/src/agents/pi-embedded-runner/reasoning-effort-utils.ts deleted file mode 100644 index e5f6acd265b..00000000000 --- a/src/agents/pi-embedded-runner/reasoning-effort-utils.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { ThinkLevel } from "../../auto-reply/thinking.js"; - -export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh"; - -export function mapThinkingLevelToReasoningEffort(thinkingLevel: ThinkLevel): ReasoningEffort { - if (thinkingLevel === "off") { - return "none"; - } - if (thinkingLevel === "adaptive") { - return "medium"; - } - if (thinkingLevel === "max") { - return "xhigh"; - } - return thinkingLevel; -} diff --git a/src/agents/pi-embedded-runner/runtime.ts b/src/agents/pi-embedded-runner/runtime.ts deleted file mode 100644 index d85ac76e296..00000000000 --- a/src/agents/pi-embedded-runner/runtime.ts +++ /dev/null @@ -1,24 +0,0 @@ -export type EmbeddedAgentRuntime = "pi" | "auto" | (string & {}); - -export function normalizeEmbeddedAgentRuntime(raw: string | undefined): EmbeddedAgentRuntime { - const value = raw?.trim(); - if (!value) { - return "pi"; - } - if (value === "pi") { - return "pi"; - } - if (value === "auto") { - return "auto"; - } - if (value === "codex-app-server") { - return "codex"; - } - return value; -} - -export function resolveEmbeddedAgentRuntime( - env: NodeJS.ProcessEnv = process.env, -): EmbeddedAgentRuntime { - return normalizeEmbeddedAgentRuntime(env.OPENCLAW_AGENT_RUNTIME?.trim()); -} diff --git a/src/agents/pi-embedded-runner/stream-payload-utils.ts b/src/agents/pi-embedded-runner/stream-payload-utils.ts deleted file mode 100644 index 1d102cb8773..00000000000 --- a/src/agents/pi-embedded-runner/stream-payload-utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; - -export function streamWithPayloadPatch( - underlying: StreamFn, - model: Parameters[0], - context: Parameters[1], - options: Parameters[2], - patchPayload: (payload: Record) => void, -): ReturnType { - const originalOnPayload = options?.onPayload; - return underlying(model, context, { - ...options, - onPayload: (payload) => { - if (payload && typeof payload === "object") { - patchPayload(payload as Record); - } - return originalOnPayload?.(payload, model); - }, - }); -} diff --git a/src/agents/pi-embedded-runner/zai-stream-wrappers.ts b/src/agents/pi-embedded-runner/zai-stream-wrappers.ts deleted file mode 100644 index c98ac5ae0e1..00000000000 --- a/src/agents/pi-embedded-runner/zai-stream-wrappers.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { streamSimple } from "@earendil-works/pi-ai"; -import { streamWithPayloadPatch } from "./stream-payload-utils.js"; - -/** - * Inject `tool_stream=true` so tool-call deltas stream in real time. - * Providers can disable this by setting `params.tool_stream=false`. - * - * @deprecated Provider-owned stream helper; do not use from third-party plugins. - */ -export function createToolStreamWrapper( - baseStreamFn: StreamFn | undefined, - enabled: boolean, -): StreamFn { - const underlying = baseStreamFn ?? streamSimple; - return (model, context, options) => { - if (!enabled) { - return underlying(model, context, options); - } - - return streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { - payloadObj.tool_stream = true; - }); - }; -} - -/** @deprecated Z.ai provider-owned stream helper; do not use from third-party plugins. */ -export const createZaiToolStreamWrapper = createToolStreamWrapper; diff --git a/src/agents/pi-embedded.runtime.ts b/src/agents/pi-embedded.runtime.ts deleted file mode 100644 index c9feed67da1..00000000000 --- a/src/agents/pi-embedded.runtime.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { - abortAndDrainEmbeddedPiRun, - abortEmbeddedPiRun, - isEmbeddedPiRunActive, - isEmbeddedPiRunStreaming, - resolveActiveEmbeddedRunSessionId, - resolveActiveEmbeddedRunSessionIdBySessionFile, - runEmbeddedPiAgent, - resolveEmbeddedSessionLane, - waitForEmbeddedPiRunEnd, -} from "./pi-embedded.js"; diff --git a/src/agents/pi-model-discovery-runtime.ts b/src/agents/pi-model-discovery-runtime.ts deleted file mode 100644 index 0adcebce8ec..00000000000 --- a/src/agents/pi-model-discovery-runtime.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { - AuthStorage, - addEnvBackedPiCredentials, - discoverAuthStorage, - discoverModels, - ModelRegistry, - normalizeDiscoveredPiModel, - resolvePiCredentialsForDiscovery, - scrubLegacyStaticAuthJsonEntriesForDiscovery, -} from "./pi-model-discovery.js"; diff --git a/src/agents/pi-model-discovery.compat.e2e.test.ts b/src/agents/pi-model-discovery.compat.e2e.test.ts deleted file mode 100644 index 51054bf50cc..00000000000 --- a/src/agents/pi-model-discovery.compat.e2e.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; - -describe("pi-model-discovery module compatibility", () => { - afterEach(() => { - vi.doUnmock("@earendil-works/pi-coding-agent"); - }); - - it("loads when InMemoryAuthStorageBackend is not exported", async () => { - vi.resetModules(); - vi.doMock("@earendil-works/pi-coding-agent", () => { - function MockAuthStorage() {} - function MockModelRegistry() {} - - return { - AuthStorage: MockAuthStorage, - ModelRegistry: MockModelRegistry, - }; - }); - - const module = await import("./pi-model-discovery.js"); - expect(typeof module.discoverAuthStorage).toBe("function"); - expect(typeof module.discoverModels).toBe("function"); - }); -}); diff --git a/src/agents/pi-model-discovery.ts b/src/agents/pi-model-discovery.ts deleted file mode 100644 index bb8e7bc7be8..00000000000 --- a/src/agents/pi-model-discovery.ts +++ /dev/null @@ -1,267 +0,0 @@ -import path from "node:path"; -import type { Api, Model } from "@earendil-works/pi-ai"; -import * as PiCodingAgent from "@earendil-works/pi-coding-agent"; -import type { - AuthStorage as PiAuthStorage, - ModelRegistry as PiModelRegistry, -} from "@earendil-works/pi-coding-agent"; -import { normalizeModelCompat } from "../plugins/provider-model-compat.js"; -import { - applyProviderResolvedModelCompatWithPlugins, - applyProviderResolvedTransportWithPlugin, - normalizeProviderResolvedModelWithPlugin, -} from "../plugins/provider-runtime.js"; -import { isRecord } from "../utils.js"; -import type { PiCredentialMap } from "./pi-auth-credentials.js"; -import { - resolvePiCredentialsForDiscovery, - scrubLegacyStaticAuthJsonEntriesForDiscovery, - type DiscoverAuthStorageOptions, -} from "./pi-auth-discovery.js"; -import { normalizeProviderId } from "./provider-id.js"; - -const PiAuthStorageClass = PiCodingAgent.AuthStorage; -const PiModelRegistryClass = PiCodingAgent.ModelRegistry; - -export { PiAuthStorageClass as AuthStorage, PiModelRegistryClass as ModelRegistry }; - -type ProviderRuntimeModelLike = Model & { - contextTokens?: number; -}; - -type DiscoveredProviderRuntimeModelLike = Omit & { - api?: string | null; -}; - -type DiscoverModelsOptions = { - providerFilter?: string; - normalizeModels?: boolean; -}; - -type InMemoryAuthStorageBackendLike = { - withLock( - update: (current: string) => { - result: T; - next?: string; - }, - ): T; -}; - -function createInMemoryAuthStorageBackend( - initialData: PiCredentialMap, -): InMemoryAuthStorageBackendLike { - let snapshot = JSON.stringify(initialData, null, 2); - return { - withLock( - update: (current: string) => { - result: T; - next?: string; - }, - ): T { - const { result, next } = update(snapshot); - if (typeof next === "string") { - snapshot = next; - } - return result; - }, - }; -} - -export function normalizeDiscoveredPiModel(value: T, agentDir: string): T { - if (!isRecord(value)) { - return value; - } - if ( - typeof value.id !== "string" || - typeof value.name !== "string" || - typeof value.provider !== "string" - ) { - return value; - } - const model = value as unknown as DiscoveredProviderRuntimeModelLike; - const pluginNormalized = - normalizeProviderResolvedModelWithPlugin({ - provider: model.provider, - context: { - provider: model.provider, - modelId: model.id, - model: model as unknown as ProviderRuntimeModelLike, - agentDir, - }, - }) ?? model; - const compatNormalized = - applyProviderResolvedModelCompatWithPlugins({ - provider: model.provider, - context: { - provider: model.provider, - modelId: model.id, - model: pluginNormalized as unknown as ProviderRuntimeModelLike, - agentDir, - }, - }) ?? pluginNormalized; - const transportNormalized = - applyProviderResolvedTransportWithPlugin({ - provider: model.provider, - context: { - provider: model.provider, - modelId: model.id, - model: compatNormalized as unknown as ProviderRuntimeModelLike, - agentDir, - }, - }) ?? compatNormalized; - if ( - !isRecord(transportNormalized) || - typeof transportNormalized.id !== "string" || - typeof transportNormalized.name !== "string" || - typeof transportNormalized.provider !== "string" || - typeof transportNormalized.api !== "string" - ) { - return value; - } - return normalizeModelCompat(transportNormalized as Model) as T; -} - -type PiModelRegistryClassLike = { - create?: (authStorage: PiAuthStorage, modelsJsonPath: string) => PiModelRegistry; - new (authStorage: PiAuthStorage, modelsJsonPath: string): PiModelRegistry; -}; - -function instantiatePiModelRegistry( - authStorage: PiAuthStorage, - modelsJsonPath: string, -): PiModelRegistry { - const Registry = PiModelRegistryClass as unknown as PiModelRegistryClassLike; - if (typeof Registry.create === "function") { - return Registry.create(authStorage, modelsJsonPath); - } - return new Registry(authStorage, modelsJsonPath); -} - -function createOpenClawModelRegistry( - authStorage: PiAuthStorage, - modelsJsonPath: string, - agentDir: string, - options?: DiscoverModelsOptions, -): PiModelRegistry { - const registry = instantiatePiModelRegistry(authStorage, modelsJsonPath); - const getAll = registry.getAll.bind(registry); - const getAvailable = registry.getAvailable.bind(registry); - const find = registry.find.bind(registry); - const refresh = registry.refresh.bind(registry); - const providerFilter = options?.providerFilter ? normalizeProviderId(options.providerFilter) : ""; - const matchesProviderFilter = (entry: Model) => - !providerFilter || normalizeProviderId(entry.provider) === providerFilter; - const shouldNormalize = options?.normalizeModels !== false; - const findCache = new Map | undefined>(); - const normalizeEntry = (entry: Model) => - shouldNormalize ? normalizeDiscoveredPiModel(entry, agentDir) : entry; - - registry.getAll = () => { - const entries = getAll().filter((entry: Model) => matchesProviderFilter(entry)); - return shouldNormalize - ? entries.map((entry: Model) => normalizeDiscoveredPiModel(entry, agentDir)) - : entries; - }; - registry.getAvailable = () => { - const entries = getAvailable().filter((entry: Model) => matchesProviderFilter(entry)); - return shouldNormalize - ? entries.map((entry: Model) => normalizeDiscoveredPiModel(entry, agentDir)) - : entries; - }; - registry.find = (provider: string, modelId: string) => { - const normalizedProvider = normalizeProviderId(provider); - const key = `${normalizedProvider}\0${modelId}`; - if (findCache.has(key)) { - return findCache.get(key); - } - const fallbackEntry = find(provider, modelId); - const resolved = fallbackEntry ? normalizeEntry(fallbackEntry) : undefined; - findCache.set(key, resolved); - return resolved; - }; - registry.refresh = () => { - findCache.clear(); - return refresh(); - }; - - return registry; -} - -function createAuthStorage(AuthStorageLike: unknown, path: string, creds: PiCredentialMap) { - const withInMemory = AuthStorageLike as { inMemory?: (data?: unknown) => unknown }; - if (typeof withInMemory.inMemory === "function") { - return withInMemory.inMemory(creds) as PiAuthStorage; - } - - const withFromStorage = AuthStorageLike as { - fromStorage?: (storage: unknown) => unknown; - }; - if (typeof withFromStorage.fromStorage === "function") { - const backendCtor = ( - PiCodingAgent as { InMemoryAuthStorageBackend?: new () => InMemoryAuthStorageBackendLike } - ).InMemoryAuthStorageBackend; - const backend = - typeof backendCtor === "function" - ? new backendCtor() - : createInMemoryAuthStorageBackend(creds); - backend.withLock(() => ({ - result: undefined, - next: JSON.stringify(creds, null, 2), - })); - return withFromStorage.fromStorage(backend) as PiAuthStorage; - } - - const withFactory = AuthStorageLike as { create?: (path: string) => unknown }; - const withRuntimeOverride = ( - typeof withFactory.create === "function" - ? withFactory.create(path) - : new (AuthStorageLike as { new (path: string): unknown })(path) - ) as PiAuthStorage & { - setRuntimeApiKey?: (provider: string, apiKey: string) => void; // pragma: allowlist secret - }; - const hasRuntimeApiKeyOverride = typeof withRuntimeOverride.setRuntimeApiKey === "function"; // pragma: allowlist secret - if (hasRuntimeApiKeyOverride) { - for (const [provider, credential] of Object.entries(creds)) { - if (credential.type === "api_key") { - withRuntimeOverride.setRuntimeApiKey(provider, credential.key); - continue; - } - withRuntimeOverride.setRuntimeApiKey(provider, credential.access); - } - } - return withRuntimeOverride; -} - -// Compatibility helpers for pi-coding-agent 0.50+ (discover* helpers removed). -export function discoverAuthStorage( - agentDir: string, - options?: DiscoverAuthStorageOptions, -): PiAuthStorage { - const credentials = - options?.skipCredentials === true ? {} : resolvePiCredentialsForDiscovery(agentDir, options); - const authPath = path.join(agentDir, "auth.json"); - if (options?.readOnly !== true) { - scrubLegacyStaticAuthJsonEntriesForDiscovery(authPath); - } - return createAuthStorage(PiAuthStorageClass, authPath, credentials); -} - -export function discoverModels( - authStorage: PiAuthStorage, - agentDir: string, - options?: DiscoverModelsOptions, -): PiModelRegistry { - return createOpenClawModelRegistry( - authStorage, - path.join(agentDir, "models.json"), - agentDir, - options, - ); -} - -export { - addEnvBackedPiCredentials, - resolvePiCredentialsForDiscovery, - scrubLegacyStaticAuthJsonEntriesForDiscovery, - type DiscoverAuthStorageOptions, -} from "./pi-auth-discovery.js"; diff --git a/src/agents/plugin-text-transforms.test.ts b/src/agents/plugin-text-transforms.test.ts index 2e0469281fb..c1542d4a9e8 100644 --- a/src/agents/plugin-text-transforms.test.ts +++ b/src/agents/plugin-text-transforms.test.ts @@ -1,10 +1,10 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import { createAssistantMessageEventStream, type AssistantMessage, type Context, type Model, -} from "@earendil-works/pi-ai"; +} from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { applyPluginTextReplacements, diff --git a/src/agents/plugin-text-transforms.ts b/src/agents/plugin-text-transforms.ts index abcb52b268c..bc8182c09ef 100644 --- a/src/agents/plugin-text-transforms.ts +++ b/src/agents/plugin-text-transforms.ts @@ -1,7 +1,6 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { streamSimple, type AssistantMessageEvent } from "@earendil-works/pi-ai"; +import { streamSimple, type AssistantMessageEvent } from "openclaw/plugin-sdk/llm"; import type { PluginTextReplacement, PluginTextTransforms } from "../plugins/cli-backend.types.js"; -import { isRecord } from "../shared/record-coerce.js"; +import type { StreamFn } from "./runtime/index.js"; import { createStreamIteratorWrapper } from "./stream-iterator-wrapper.js"; export function mergePluginTextTransforms( @@ -32,6 +31,10 @@ export function applyPluginTextReplacements( return next; } +function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === "object" && !Array.isArray(value)); +} + function transformContentText(content: unknown, replacements?: PluginTextReplacement[]): unknown { if (typeof content === "string") { return applyPluginTextReplacements(content, replacements); diff --git a/src/agents/prompt-surface.ts b/src/agents/prompt-surface.ts index 31d4e897abe..5c33226958b 100644 --- a/src/agents/prompt-surface.ts +++ b/src/agents/prompt-surface.ts @@ -17,9 +17,9 @@ export function buildOpenClawToolFallbackText(params: { execToolName: string; processToolName: string; }): string { - if (params.surface === "pi_main") { + if (params.surface === "openclaw_main" || params.surface === "pi_main") { return [ - "Pi lists the standard tools above. This runtime enables:", + "OpenClaw lists the standard tools above. This runtime enables:", "- grep: search file contents for patterns", "- find: find files by glob pattern", "- ls: list directory contents", @@ -47,7 +47,7 @@ export function shouldRenderOpenClawToolWorkflowHints(params: { surface: AgentPromptSurfaceKind; hasToolList: boolean; }): boolean { - return params.surface === "pi_main"; + return params.surface === "openclaw_main" || params.surface === "pi_main"; } export function resolveAgentPromptSurfaceForSessionKey( @@ -56,5 +56,5 @@ export function resolveAgentPromptSurfaceForSessionKey( if (sessionKey && isAcpSessionKey(sessionKey)) { return "acp_backend"; } - return sessionKey && isSubagentSessionKey(sessionKey) ? "subagent" : "pi_main"; + return sessionKey && isSubagentSessionKey(sessionKey) ? "subagent" : "openclaw_main"; } diff --git a/src/agents/provider-local-service.test.ts b/src/agents/provider-local-service.test.ts index 9b08f7e27af..d75bf321312 100644 --- a/src/agents/provider-local-service.test.ts +++ b/src/agents/provider-local-service.test.ts @@ -2,7 +2,7 @@ import fs from "node:fs/promises"; import net from "node:net"; import os from "node:os"; import path from "node:path"; -import type { Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { afterEach, describe, expect, it } from "vitest"; import { attachModelProviderLocalService, diff --git a/src/agents/provider-local-service.ts b/src/agents/provider-local-service.ts index 68c8b0dbc2f..a4b32f4d554 100644 --- a/src/agents/provider-local-service.ts +++ b/src/agents/provider-local-service.ts @@ -1,6 +1,6 @@ import { spawn, type ChildProcess } from "node:child_process"; import path from "node:path"; -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import type { ModelProviderLocalServiceConfig } from "../config/types.models.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; @@ -55,7 +55,7 @@ export function getModelProviderLocalService( } export async function ensureModelProviderLocalService( - model: Model, + model: Model, probeHeaders?: HeadersInit, signal?: AbortSignal | null, ): Promise { @@ -176,7 +176,7 @@ function sortedStringRecord(record: Record | undefined): Record< } function buildHealthProbeHeaders( - model: Model, + model: Model, requestHeaders: HeadersInit | undefined, ): Headers | undefined { const headers = new Headers(); diff --git a/src/agents/provider-request-config.ts b/src/agents/provider-request-config.ts index c5f0b595ca1..f0b41e279c0 100644 --- a/src/agents/provider-request-config.ts +++ b/src/agents/provider-request-config.ts @@ -1,4 +1,4 @@ -import type { Api } from "@earendil-works/pi-ai"; +import type { Api } from "openclaw/plugin-sdk/llm"; import type { ModelDefinitionConfig } from "../config/types.js"; import type { ConfiguredModelProviderRequest, diff --git a/src/agents/provider-transport-fetch.test.ts b/src/agents/provider-transport-fetch.test.ts index 6c7fc0fc7cf..4da86286be0 100644 --- a/src/agents/provider-transport-fetch.test.ts +++ b/src/agents/provider-transport-fetch.test.ts @@ -1,5 +1,5 @@ -import type { Model } from "@earendil-works/pi-ai"; import { Stream } from "openai/streaming"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { buildGuardedModelFetch } from "./provider-transport-fetch.js"; diff --git a/src/agents/provider-transport-stream.test.ts b/src/agents/provider-transport-stream.test.ts index 49bdf6e8c40..8409689ece9 100644 --- a/src/agents/provider-transport-stream.test.ts +++ b/src/agents/provider-transport-stream.test.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { attachModelProviderLocalService } from "./provider-local-service.js"; import { attachModelProviderRequestTransport } from "./provider-request-config.js"; @@ -197,7 +197,7 @@ describe("provider transport stream contracts", () => { expect(preparedModel.id).toBe("google/gemma-4-E2B-it"); }); - it("keeps Codex defaults on the OpenClaw transport until PI preserves attribution", () => { + it("keeps Codex defaults on the OpenClaw transport until OpenClaw preserves attribution", () => { const model = buildModel("openai-codex-responses", { id: "gpt-5.4", provider: "openai-codex", diff --git a/src/agents/realtime-bootstrap-context.ts b/src/agents/realtime-bootstrap-context.ts index bfa94fb3733..8fa5f12a2a6 100644 --- a/src/agents/realtime-bootstrap-context.ts +++ b/src/agents/realtime-bootstrap-context.ts @@ -3,7 +3,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resolveUserPath, truncateUtf16Safe } from "../utils.js"; import { resolveAgentWorkspaceDir } from "./agent-scope.js"; import { resolveBootstrapFilesForRun } from "./bootstrap-files.js"; -import { buildBootstrapContextFiles } from "./pi-embedded-helpers.js"; +import { buildBootstrapContextFiles } from "./embedded-agent-helpers.js"; import { DEFAULT_IDENTITY_FILENAME, DEFAULT_SOUL_FILENAME, diff --git a/src/agents/run-cleanup-timeout.test.ts b/src/agents/run-cleanup-timeout.test.ts index 4799055c524..d2136aa36d2 100644 --- a/src/agents/run-cleanup-timeout.test.ts +++ b/src/agents/run-cleanup-timeout.test.ts @@ -46,7 +46,7 @@ describe("agent cleanup timeout", () => { const result = runAgentCleanupStep({ runId: "run-trajectory", sessionId: "session-trajectory", - step: "pi-trajectory-flush", + step: "openclaw-trajectory-flush", cleanup, log, env: { @@ -62,7 +62,7 @@ describe("agent cleanup timeout", () => { expect(cleanup).toHaveBeenCalledTimes(1); expect(log.warn).toHaveBeenCalledWith( - "agent cleanup timed out: runId=run-trajectory sessionId=session-trajectory step=pi-trajectory-flush timeoutMs=25000", + "agent cleanup timed out: runId=run-trajectory sessionId=session-trajectory step=openclaw-trajectory-flush timeoutMs=25000", ); }); @@ -72,7 +72,7 @@ describe("agent cleanup timeout", () => { const result = runAgentCleanupStep({ runId: "run-trajectory", sessionId: "session-trajectory", - step: "pi-trajectory-flush", + step: "openclaw-trajectory-flush", cleanup, log, timeoutMs: 5, @@ -83,7 +83,7 @@ describe("agent cleanup timeout", () => { await expect(result).resolves.toBeUndefined(); expect(log.warn).toHaveBeenCalledWith( - "agent cleanup timed out: runId=run-trajectory sessionId=session-trajectory step=pi-trajectory-flush timeoutMs=5 details=pendingWrites=2 queuedBytes=128 activeOperation=file-append", + "agent cleanup timed out: runId=run-trajectory sessionId=session-trajectory step=openclaw-trajectory-flush timeoutMs=5 details=pendingWrites=2 queuedBytes=128 activeOperation=file-append", ); }); @@ -94,7 +94,7 @@ describe("agent cleanup timeout", () => { const result = runAgentCleanupStep({ runId: "run-trajectory", sessionId: "session-trajectory", - step: "pi-trajectory-flush", + step: "agent-trajectory-flush", cleanup, log, timeoutMs: 5, @@ -108,7 +108,7 @@ describe("agent cleanup timeout", () => { expect(message).toContain(" details=queuedBytes="); expect(message).toContain("...[truncated]"); expect(message.length).toBeLessThan( - "agent cleanup timed out: runId=run-trajectory sessionId=session-trajectory step=pi-trajectory-flush timeoutMs=5 details=" + "agent cleanup timed out: runId=run-trajectory sessionId=session-trajectory step=agent-trajectory-flush timeoutMs=5 details=" .length + CLEANUP_TIMEOUT_DETAILS_MAX_CHARS + 1, @@ -121,7 +121,7 @@ describe("agent cleanup timeout", () => { const result = runAgentCleanupStep({ runId: "run-trajectory", sessionId: "session-trajectory", - step: "pi-trajectory-flush", + step: "openclaw-trajectory-flush", cleanup, log, timeoutMs: 5, @@ -134,7 +134,7 @@ describe("agent cleanup timeout", () => { await expect(result).resolves.toBeUndefined(); expect(log.warn).toHaveBeenCalledWith( - "agent cleanup timed out: runId=run-trajectory sessionId=session-trajectory step=pi-trajectory-flush timeoutMs=5 detailsError=details unavailable", + "agent cleanup timed out: runId=run-trajectory sessionId=session-trajectory step=openclaw-trajectory-flush timeoutMs=5 detailsError=details unavailable", ); }); @@ -144,7 +144,7 @@ describe("agent cleanup timeout", () => { const result = runAgentCleanupStep({ runId: "run-trajectory", sessionId: "session-trajectory", - step: "pi-trajectory-flush", + step: "agent-trajectory-flush", cleanup, log, timeoutMs: 5, @@ -160,7 +160,7 @@ describe("agent cleanup timeout", () => { expect(message).toContain(" detailsError=details unavailable"); expect(message).toContain("...[truncated]"); expect(message.length).toBeLessThan( - "agent cleanup timed out: runId=run-trajectory sessionId=session-trajectory step=pi-trajectory-flush timeoutMs=5 detailsError=" + "agent cleanup timed out: runId=run-trajectory sessionId=session-trajectory step=agent-trajectory-flush timeoutMs=5 detailsError=" .length + CLEANUP_TIMEOUT_DETAILS_MAX_CHARS + 1, @@ -192,7 +192,7 @@ describe("agent cleanup timeout", () => { it("prefers explicit cleanup timeout values over environment overrides", () => { expect( resolveAgentCleanupStepTimeoutMs({ - step: "pi-trajectory-flush", + step: "openclaw-trajectory-flush", timeoutMs: 2_000, env: { OPENCLAW_TRAJECTORY_FLUSH_TIMEOUT_MS: "25000", @@ -205,7 +205,7 @@ describe("agent cleanup timeout", () => { it("keeps explicit zero cleanup timeouts as a one millisecond timeout", () => { expect( resolveAgentCleanupStepTimeoutMs({ - step: "pi-trajectory-flush", + step: "openclaw-trajectory-flush", timeoutMs: 0, env: { OPENCLAW_TRAJECTORY_FLUSH_TIMEOUT_MS: "25000", @@ -217,7 +217,7 @@ describe("agent cleanup timeout", () => { it("ignores invalid cleanup timeout environment values", () => { expect( resolveAgentCleanupStepTimeoutMs({ - step: "pi-trajectory-flush", + step: "openclaw-trajectory-flush", env: { OPENCLAW_TRAJECTORY_FLUSH_TIMEOUT_MS: "0", OPENCLAW_AGENT_CLEANUP_TIMEOUT_MS: "not-a-number", diff --git a/src/agents/run-cleanup-timeout.ts b/src/agents/run-cleanup-timeout.ts index afec3e67ead..58fe0383684 100644 --- a/src/agents/run-cleanup-timeout.ts +++ b/src/agents/run-cleanup-timeout.ts @@ -64,7 +64,7 @@ export function resolveAgentCleanupStepTimeoutMs(params: { } const env = params.env ?? process.env; - if (params.step === "pi-trajectory-flush") { + if (params.step === "openclaw-trajectory-flush") { const trajectoryTimeoutMs = parseTimeoutEnvValue(env[TRAJECTORY_FLUSH_TIMEOUT_ENV]); if (trajectoryTimeoutMs !== undefined) { return trajectoryTimeoutMs; diff --git a/src/agents/runtime-plan/auth.ts b/src/agents/runtime-plan/auth.ts index 57565755acf..630d56fa4a9 100644 --- a/src/agents/runtime-plan/auth.ts +++ b/src/agents/runtime-plan/auth.ts @@ -1,6 +1,6 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { shouldRouteOpenAIPiThroughCodexAuthProvider } from "../openai-codex-routing.js"; -import { normalizeEmbeddedAgentRuntime } from "../pi-embedded-runner/runtime.js"; +import { normalizeOptionalAgentRuntimeId } from "../agent-runtime-id.js"; +import { shouldRouteOpenAIThroughCodexAuthProvider } from "../openai-codex-routing.js"; import { resolveProviderIdForAuth } from "../provider-auth-aliases.js"; import type { AgentRuntimeAuthPlan } from "./types.js"; @@ -11,8 +11,8 @@ function resolveHarnessAuthProvider(params: { harnessId?: string; harnessRuntime?: string; }): string | undefined { - const harnessId = normalizeEmbeddedAgentRuntime(params.harnessId); - const runtime = normalizeEmbeddedAgentRuntime(params.harnessRuntime); + const harnessId = normalizeOptionalAgentRuntimeId(params.harnessId); + const runtime = normalizeOptionalAgentRuntimeId(params.harnessRuntime); return harnessId === "codex" || runtime === "codex" ? CODEX_HARNESS_AUTH_PROVIDER : undefined; } @@ -48,7 +48,7 @@ export function buildAgentRuntimeAuthPlan(params: { (harnessProviderForAuth === CODEX_HARNESS_AUTH_PROVIDER && authProfileProviderForAuth === OPENAI_PROVIDER && params.authProfileMode === "api_key")); - const openAIPiCanForwardCodexProfile = shouldRouteOpenAIPiThroughCodexAuthProvider({ + const openAICanForwardCodexProfile = shouldRouteOpenAIThroughCodexAuthProvider({ provider: providerForAuth, harnessRuntime: params.harnessRuntime, agentHarnessId: params.harnessId, @@ -60,7 +60,7 @@ export function buildAgentRuntimeAuthPlan(params: { const providerCanForwardProfile = !harnessProviderForAuth && providerForAuth === authProfileProviderForAuth; const canForwardProfile = - providerCanForwardProfile || harnessCanForwardProfile || openAIPiCanForwardCodexProfile; + providerCanForwardProfile || harnessCanForwardProfile || openAICanForwardCodexProfile; return { providerForAuth, diff --git a/src/agents/runtime-plan/build.test.ts b/src/agents/runtime-plan/build.test.ts index 1c58d845f96..88468243c19 100644 --- a/src/agents/runtime-plan/build.test.ts +++ b/src/agents/runtime-plan/build.test.ts @@ -262,13 +262,13 @@ describe("AgentRuntimePlan", () => { expect(plan.auth.forwardedAuthProfileId).toBeUndefined(); }); - it("forwards OpenAI Codex profiles for explicit OpenAI PI runs", () => { + it("forwards OpenAI Codex profiles for explicit OpenAI OpenClaw runs", () => { const plan = buildAgentRuntimePlan({ provider: "openai", modelId: "gpt-5.4", modelApi: "openai-responses", - harnessId: "pi", - harnessRuntime: "pi", + harnessId: "openclaw", + harnessRuntime: "openclaw", authProfileProvider: "openai-codex", sessionAuthProfileId: "openai-codex:work", config: {}, diff --git a/src/agents/runtime-plan/build.ts b/src/agents/runtime-plan/build.ts index 9b21e768816..c066ba819e9 100644 --- a/src/agents/runtime-plan/build.ts +++ b/src/agents/runtime-plan/build.ts @@ -1,4 +1,3 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; import type { TSchema } from "typebox"; import type { ThinkLevel } from "../../auto-reply/thinking.js"; import { isSilentReplyPayloadText, SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js"; @@ -18,12 +17,13 @@ import { resolveProviderTextTransforms, transformProviderSystemPrompt, } from "../../plugins/provider-runtime.js"; -import { resolvePreparedExtraParams } from "../pi-embedded-runner/extra-params.js"; -import { classifyEmbeddedPiRunResultForModelFallback } from "../pi-embedded-runner/result-fallback-classifier.js"; +import { resolvePreparedExtraParams } from "../embedded-agent-runner/extra-params.js"; +import { classifyEmbeddedAgentRunResultForModelFallback } from "../embedded-agent-runner/result-fallback-classifier.js"; import { logProviderToolSchemaDiagnostics, normalizeProviderToolSchemas, -} from "../pi-embedded-runner/tool-schema-runtime.js"; +} from "../embedded-agent-runner/tool-schema-runtime.js"; +import type { AgentTool } from "../runtime/index.js"; import { resolveTranscriptPolicy } from "../transcript-policy.js"; import { buildAgentRuntimeAuthPlan } from "./auth.js"; import type { @@ -126,7 +126,7 @@ export function buildAgentRuntimeDeliveryPlan( export function buildAgentRuntimeOutcomePlan(): AgentRuntimeOutcomePlan { return { - classifyRunResult: classifyEmbeddedPiRunResultForModelFallback, + classifyRunResult: classifyEmbeddedAgentRunResultForModelFallback, }; } diff --git a/src/agents/runtime-plan/tools.diagnostics.test.ts b/src/agents/runtime-plan/tools.diagnostics.test.ts index 5b243f26f61..49b495cc0c0 100644 --- a/src/agents/runtime-plan/tools.diagnostics.test.ts +++ b/src/agents/runtime-plan/tools.diagnostics.test.ts @@ -5,7 +5,7 @@ const mocks = vi.hoisted(() => ({ normalizeProviderToolSchemas: vi.fn((params: { tools: unknown[] }) => params.tools), })); -vi.mock("../pi-embedded-runner/tool-schema-runtime.js", () => ({ +vi.mock("../embedded-agent-runner/tool-schema-runtime.js", () => ({ logProviderToolSchemaDiagnostics: mocks.logProviderToolSchemaDiagnostics, normalizeProviderToolSchemas: mocks.normalizeProviderToolSchemas, })); diff --git a/src/agents/runtime-plan/tools.test.ts b/src/agents/runtime-plan/tools.test.ts index 12b48cdce22..cb634cabeb7 100644 --- a/src/agents/runtime-plan/tools.test.ts +++ b/src/agents/runtime-plan/tools.test.ts @@ -1,4 +1,4 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; +import type { AgentTool } from "openclaw/plugin-sdk/agent-core"; import { createNativeOpenAIResponsesModel, createParameterFreeTool, @@ -13,7 +13,7 @@ const mocks = vi.hoisted(() => ({ normalizeProviderToolSchemas: vi.fn(), })); -vi.mock("../pi-embedded-runner/tool-schema-runtime.js", () => ({ +vi.mock("../embedded-agent-runner/tool-schema-runtime.js", () => ({ logProviderToolSchemaDiagnostics: mocks.logProviderToolSchemaDiagnostics, normalizeProviderToolSchemas: mocks.normalizeProviderToolSchemas, })); diff --git a/src/agents/runtime-plan/tools.ts b/src/agents/runtime-plan/tools.ts index 824b9252920..ed50e192408 100644 --- a/src/agents/runtime-plan/tools.ts +++ b/src/agents/runtime-plan/tools.ts @@ -1,11 +1,11 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; import type { TSchema } from "typebox"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { ProviderRuntimeModel } from "../../plugins/provider-runtime-model.types.js"; import { logProviderToolSchemaDiagnostics, normalizeProviderToolSchemas, -} from "../pi-embedded-runner/tool-schema-runtime.js"; +} from "../embedded-agent-runner/tool-schema-runtime.js"; +import type { AgentTool } from "../runtime/index.js"; import type { AgentRuntimePlan } from "./types.js"; type AgentRuntimeToolPolicyParams = { diff --git a/src/agents/runtime-plan/types.compat.test.ts b/src/agents/runtime-plan/types.compat.test.ts index 725a6f5e6fc..069d9a89f7b 100644 --- a/src/agents/runtime-plan/types.compat.test.ts +++ b/src/agents/runtime-plan/types.compat.test.ts @@ -1,7 +1,7 @@ import { describe, expectTypeOf, it } from "vitest"; import type { ReplyPayload } from "../../auto-reply/reply-payload.js"; import type { ThinkLevel } from "../../auto-reply/thinking.js"; -import type { FailoverReason } from "../pi-embedded-helpers/types.js"; +import type { FailoverReason } from "../embedded-agent-helpers/types.js"; import type { PromptMode } from "../system-prompt.types.js"; import type { buildAgentRuntimeDeliveryPlan, buildAgentRuntimePlan } from "./build.js"; import type { diff --git a/src/agents/runtime-plan/types.test.ts b/src/agents/runtime-plan/types.test.ts index 5b011239979..234d490867c 100644 --- a/src/agents/runtime-plan/types.test.ts +++ b/src/agents/runtime-plan/types.test.ts @@ -8,7 +8,7 @@ const concreteRuntimePolicyImportPatterns = [ /from\s+["'][^"']*auto-reply(?:\/|\.js|["'])/, /from\s+["'](?:[^"']*\/)?config(?:\/|\.js|["'])/, /from\s+["'](?:[^"']*\/)?plugins(?:\/|\.js|["'])/, - /from\s+["'][^"']*pi-embedded-/, + /from\s+["'][^"']*embedded-agent-/, /from\s+["'][^"']*transcript-policy(?:\.[^/"']+)?(?:\/|\.js|["'])/, /from\s+["'][^"']*system-prompt(?:\.[^/"']+)?(?:\/|\.js|["'])/, ]; diff --git a/src/agents/runtime-plan/types.ts b/src/agents/runtime-plan/types.ts index 115074bcea3..17ff6d58d8d 100644 --- a/src/agents/runtime-plan/types.ts +++ b/src/agents/runtime-plan/types.ts @@ -1,5 +1,5 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; import type { TSchema } from "typebox"; +import type { AgentTool } from "../runtime/index.js"; export type AgentRuntimeTransport = "sse" | "websocket" | "auto"; diff --git a/src/agents/sandbox-paths.windows-drive-resolve.test.ts b/src/agents/sandbox-paths.windows-drive-resolve.test.ts index 2b5caac1802..4af1ce9c275 100644 --- a/src/agents/sandbox-paths.windows-drive-resolve.test.ts +++ b/src/agents/sandbox-paths.windows-drive-resolve.test.ts @@ -1,6 +1,6 @@ import path from "node:path"; import { describe, expect, it } from "vitest"; -import { resolveToolPathAgainstWorkspaceRoot } from "./pi-tools.read.js"; +import { resolveToolPathAgainstWorkspaceRoot } from "./agent-tools.read.js"; import { resolveSandboxInputPath } from "./sandbox-paths.js"; describe("resolveSandboxInputPath (Windows drive paths under POSIX rules)", () => { diff --git a/src/agents/sandbox-tool-policy.test.ts b/src/agents/sandbox-tool-policy.test.ts index 9360e6bf30b..1a44ac80541 100644 --- a/src/agents/sandbox-tool-policy.test.ts +++ b/src/agents/sandbox-tool-policy.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; -import { resolveEffectiveToolPolicy } from "./pi-tools.policy.js"; +import { resolveEffectiveToolPolicy } from "./agent-tools.policy.js"; import { pickSandboxToolPolicy } from "./sandbox-tool-policy.js"; import { resolveEffectiveToolFsRootExpansionAllowed } from "./tool-fs-policy.js"; diff --git a/src/agents/schema-normalization-runtime-contract.test.ts b/src/agents/schema-normalization-runtime-contract.test.ts index 54e5049035d..3832a5dc5c4 100644 --- a/src/agents/schema-normalization-runtime-contract.test.ts +++ b/src/agents/schema-normalization-runtime-contract.test.ts @@ -1,4 +1,4 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import { createNativeOpenAIResponsesModel, createParameterFreeTool, @@ -7,9 +7,9 @@ import { normalizedParameterFreeSchema, } from "openclaw/plugin-sdk/agent-runtime-test-contracts"; import { describe, expect, it } from "vitest"; +import { createOpenAIResponsesContextManagementWrapper } from "../llm/providers/stream-wrappers/openai.js"; import { buildProviderToolCompatFamilyHooks } from "../plugin-sdk/provider-tools.js"; import { buildOpenAIResponsesParams } from "./openai-transport-stream.js"; -import { createOpenAIResponsesContextManagementWrapper } from "./pi-embedded-runner/openai-stream-wrappers.js"; describe("OpenAI transport schema normalization runtime contract", () => { it("keeps HTTP Responses strict decisions stable for the same tool set", () => { diff --git a/src/agents/session-file-repair.ts b/src/agents/session-file-repair.ts index 9b7e91b8676..d3ad5f5b972 100644 --- a/src/agents/session-file-repair.ts +++ b/src/agents/session-file-repair.ts @@ -1,8 +1,8 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { replaceFileAtomic } from "../infra/replace-file.js"; +import type { AgentMessage } from "./runtime/index.js"; import { makeMissingToolResult } from "./session-transcript-repair.js"; import { STREAM_ERROR_FALLBACK_TEXT } from "./stream-message-shared.js"; import { extractToolCallsFromAssistant, extractToolResultId } from "./tool-call-id.js"; diff --git a/src/agents/session-raw-append-message.ts b/src/agents/session-raw-append-message.ts index 4af375a377d..2fd1a260fa3 100644 --- a/src/agents/session-raw-append-message.ts +++ b/src/agents/session-raw-append-message.ts @@ -1,4 +1,4 @@ -import type { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { SessionManager } from "./sessions/index.js"; const RAW_APPEND_MESSAGE = Symbol("openclaw.session.rawAppendMessage"); diff --git a/src/agents/session-runtime-compat.ts b/src/agents/session-runtime-compat.ts new file mode 100644 index 00000000000..903a9bd3acc --- /dev/null +++ b/src/agents/session-runtime-compat.ts @@ -0,0 +1,56 @@ +import type { SessionEntry } from "../config/sessions.js"; +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { isDefaultAgentRuntimeId } from "./agent-runtime-id.js"; +import { normalizeOptionalAgentRuntimeId } from "./agent-runtime-id.js"; +import { listLegacyRuntimeModelProviderAliases } from "./model-runtime-aliases.js"; +import { resolveContextConfigProviderForRuntime } from "./openai-codex-routing.js"; + +export type SessionRuntimeCompatEntry = Pick< + SessionEntry, + "agentHarnessId" | "agentRuntimeOverride" +>; + +export function resolvePersistedSessionRuntimeId( + entry?: SessionRuntimeCompatEntry, +): string | undefined { + const runtimeOverride = normalizeOptionalAgentRuntimeId(entry?.agentRuntimeOverride); + if (runtimeOverride && !isDefaultAgentRuntimeId(runtimeOverride)) { + return runtimeOverride; + } + return normalizeOptionalAgentRuntimeId(entry?.agentHarnessId); +} + +export function resolveSessionRuntimeOverrideForProvider(params: { + provider: string; + entry?: Pick; +}): string | undefined { + const provider = normalizeLowercaseStringOrEmpty(params.provider); + const runtime = normalizeOptionalAgentRuntimeId(params.entry?.agentRuntimeOverride); + if (!runtime || isDefaultAgentRuntimeId(runtime)) { + return undefined; + } + if (runtime === "openclaw") { + return "openclaw"; + } + if (provider === "openai" && runtime === "codex") { + return "codex"; + } + return listLegacyRuntimeModelProviderAliases().find( + (alias) => + normalizeLowercaseStringOrEmpty(alias.provider) === provider && + normalizeLowercaseStringOrEmpty(alias.runtime) === runtime, + )?.runtime; +} + +export function resolveContextConfigProviderForSessionRuntime(params: { + provider: string; + entry?: SessionRuntimeCompatEntry; +}): string | undefined { + const runtimeId = resolvePersistedSessionRuntimeId(params.entry); + return runtimeId + ? resolveContextConfigProviderForRuntime({ + provider: params.provider, + runtimeId, + }) + : undefined; +} diff --git a/src/agents/session-suspension.ts b/src/agents/session-suspension.ts index df8a8603e29..5b6905c8a05 100644 --- a/src/agents/session-suspension.ts +++ b/src/agents/session-suspension.ts @@ -6,7 +6,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { setCommandLaneConcurrency } from "../process/command-queue.js"; import { resolveStoredSessionKeyForSessionId } from "./command/session.js"; -import type { FailoverReason } from "./pi-embedded-helpers/types.js"; +import type { FailoverReason } from "./embedded-agent-helpers/types.js"; const log = createSubsystemLogger("session-suspension"); diff --git a/src/agents/session-tool-result-guard-wrapper.ts b/src/agents/session-tool-result-guard-wrapper.ts index d210b42a412..e6938627a73 100644 --- a/src/agents/session-tool-result-guard-wrapper.ts +++ b/src/agents/session-tool-result-guard-wrapper.ts @@ -1,5 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { SessionManager } from "@earendil-works/pi-coding-agent"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; import { @@ -10,8 +8,10 @@ import { mergePreparedUserTurnMessageForRuntime, type PersistedUserTurnMessage, } from "../sessions/user-turn-transcript.js"; -import { resolveLiveToolResultMaxChars } from "./pi-embedded-runner/tool-result-truncation.js"; +import { resolveLiveToolResultMaxChars } from "./embedded-agent-runner/tool-result-truncation.js"; +import type { AgentMessage } from "./runtime/index.js"; import { installSessionToolResultGuard } from "./session-tool-result-guard.js"; +import type { SessionManager } from "./sessions/index.js"; import { redactTranscriptMessage } from "./transcript-redact.js"; type GuardedSessionManager = SessionManager & { @@ -55,9 +55,7 @@ export function guardSessionManager( const hookRunner = getGlobalHookRunner(); let pendingPreparedUserTurnMessage = opts?.preparedUserTurnMessage; - const beforeMessageWrite = (event: { - message: import("@earendil-works/pi-agent-core").AgentMessage; - }) => { + const beforeMessageWrite = (event: { message: AgentMessage }) => { let message = event.message; let changed = false; if (hookRunner?.hasHooks("before_message_write")) { diff --git a/src/agents/session-tool-result-guard.test.ts b/src/agents/session-tool-result-guard.test.ts index c37d9153524..c7b30dc8ec7 100644 --- a/src/agents/session-tool-result-guard.test.ts +++ b/src/agents/session-tool-result-guard.test.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import { describe, expect, it } from "vitest"; import { installSessionToolResultGuard } from "./session-tool-result-guard.js"; import { castAgentMessage } from "./test-helpers/agent-message-fixtures.js"; @@ -173,7 +173,7 @@ describe("installSessionToolResultGuard", () => { expectPersistedRoles(sm, ["assistant", "toolResult"]); }); - it("applies pi-style count-based truncation wording when persisting oversized tool results", () => { + it("applies count-based truncation wording when persisting oversized tool results", () => { const sm = SessionManager.inMemory(); installSessionToolResultGuard(sm); diff --git a/src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts b/src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts index 3265cf4f462..8a553c02639 100644 --- a/src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts +++ b/src/agents/session-tool-result-guard.tool-result-persist-hook.test.ts @@ -1,8 +1,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import { describe, expect, it, afterEach, vi } from "vitest"; import { initializeGlobalHookRunner, diff --git a/src/agents/session-tool-result-guard.transcript-events.test.ts b/src/agents/session-tool-result-guard.transcript-events.test.ts index f3bd3b59a37..af3521ead5a 100644 --- a/src/agents/session-tool-result-guard.transcript-events.test.ts +++ b/src/agents/session-tool-result-guard.transcript-events.test.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, describe, expect, it, vi } from "vitest"; import { onSessionTranscriptUpdate, diff --git a/src/agents/session-tool-result-guard.ts b/src/agents/session-tool-result-guard.ts index 165b181facf..7c0637a77fc 100644 --- a/src/agents/session-tool-result-guard.ts +++ b/src/agents/session-tool-result-guard.ts @@ -1,5 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { SessionManager } from "@earendil-works/pi-coding-agent"; import { boundedJsonUtf8Bytes, firstEnumerableOwnKeys, @@ -17,17 +15,19 @@ import type { } from "../plugins/types.js"; import { emitSessionTranscriptUpdate } from "../sessions/transcript-events.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; -import { formatContextLimitTruncationNotice } from "./pi-embedded-runner/context-truncation-notice.js"; +import { formatContextLimitTruncationNotice } from "./embedded-agent-runner/context-truncation-notice.js"; import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS, truncateToolResultMessage, -} from "./pi-embedded-runner/tool-result-truncation.js"; +} from "./embedded-agent-runner/tool-result-truncation.js"; +import type { AgentMessage } from "./runtime/index.js"; import { getRawSessionAppendMessage, setRawSessionAppendMessage, } from "./session-raw-append-message.js"; import { createPendingToolCallState } from "./session-tool-result-state.js"; import { makeMissingToolResult, sanitizeToolCallInputs } from "./session-transcript-repair.js"; +import type { SessionManager } from "./sessions/index.js"; import { extractToolCallsFromAssistant, extractToolResultId } from "./tool-call-id.js"; /** diff --git a/src/agents/session-transcript-repair.attachments.test.ts b/src/agents/session-transcript-repair.attachments.test.ts index 864e1b277bc..ba756435e98 100644 --- a/src/agents/session-transcript-repair.attachments.test.ts +++ b/src/agents/session-transcript-repair.attachments.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, it, expect } from "vitest"; import { sanitizeToolCallInputs } from "./session-transcript-repair.js"; import { castAgentMessage, castAgentMessages } from "./test-helpers/agent-message-fixtures.js"; diff --git a/src/agents/session-transcript-repair.test.ts b/src/agents/session-transcript-repair.test.ts index 2b7b7acba53..cfda4ac07a7 100644 --- a/src/agents/session-transcript-repair.test.ts +++ b/src/agents/session-transcript-repair.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; import { sanitizeToolCallInputs, diff --git a/src/agents/session-transcript-repair.ts b/src/agents/session-transcript-repair.ts index d6cf3600eac..53c0b325bca 100644 --- a/src/agents/session-transcript-repair.ts +++ b/src/agents/session-transcript-repair.ts @@ -1,9 +1,9 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import { hasNonEmptyString as hasNonEmptyStringField, normalizeOptionalString, readStringValue, } from "../shared/string-coerce.js"; +import type { AgentMessage } from "./runtime/index.js"; import { extractToolCallsFromAssistant, extractToolResultId, diff --git a/src/agents/sessions/agent-session-runtime.ts b/src/agents/sessions/agent-session-runtime.ts new file mode 100644 index 00000000000..56e430a4771 --- /dev/null +++ b/src/agents/sessions/agent-session-runtime.ts @@ -0,0 +1,441 @@ +import { copyFileSync, existsSync, mkdirSync } from "node:fs"; +import { basename, join, resolve } from "node:path"; +import type { + AgentSessionRuntimeDiagnostic, + AgentSessionServices, +} from "./agent-session-services.js"; +import type { AgentSession } from "./agent-session.js"; +import type { + ReplacedSessionContext, + SessionShutdownEvent, + SessionStartEvent, +} from "./extensions/index.js"; +import { emitSessionShutdownEvent } from "./extensions/runner.js"; +import type { CreateAgentSessionResult } from "./sdk.js"; +import { assertSessionCwdExists } from "./session-cwd.js"; +import { SessionManager } from "./session-manager.js"; + +/** + * Result returned by runtime creation. + * + * The caller gets the created session, its cwd-bound services, and all + * diagnostics collected during setup. + */ +export interface CreateAgentSessionRuntimeResult extends CreateAgentSessionResult { + services: AgentSessionServices; + diagnostics: AgentSessionRuntimeDiagnostic[]; +} + +/** + * Creates a full runtime for a target cwd and session manager. + * + * The factory closes over process-global fixed inputs, recreates cwd-bound + * services for the effective cwd, resolves session options against those + * services, and finally creates the AgentSession. + */ +export type CreateAgentSessionRuntimeFactory = (options: { + cwd: string; + agentDir: string; + sessionManager: SessionManager; + sessionStartEvent?: SessionStartEvent; +}) => Promise; + +/** + * Thrown when /import references a JSONL file path that does not exist. + */ +export class SessionImportFileNotFoundError extends Error { + readonly filePath: string; + + constructor(filePath: string) { + super(`File not found: ${filePath}`); + this.name = "SessionImportFileNotFoundError"; + this.filePath = filePath; + } +} + +function extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string { + if (typeof content === "string") { + return content; + } + + return content + .filter( + (part): part is { type: "text"; text: string } => + part.type === "text" && typeof part.text === "string", + ) + .map((part) => part.text) + .join(""); +} + +/** + * Owns the current AgentSession plus its cwd-bound services. + * + * Session replacement methods tear down the current runtime first, then create + * and apply the next runtime. If creation fails, the error is propagated to the + * caller. The caller is responsible for user-facing error handling. + */ +export class AgentSessionRuntime { + private rebindSession?: (session: AgentSession) => Promise; + private beforeSessionInvalidate?: () => void; + private currentSession: AgentSession; + private runtimeServices: AgentSessionServices; + private readonly createRuntime: CreateAgentSessionRuntimeFactory; + private runtimeDiagnostics: AgentSessionRuntimeDiagnostic[]; + private fallbackMessage?: string; + + constructor( + session: AgentSession, + services: AgentSessionServices, + createRuntime: CreateAgentSessionRuntimeFactory, + diagnostics: AgentSessionRuntimeDiagnostic[] = [], + modelFallbackMessage?: string, + ) { + this.currentSession = session; + this.runtimeServices = services; + this.createRuntime = createRuntime; + this.runtimeDiagnostics = diagnostics; + this.fallbackMessage = modelFallbackMessage; + } + + get services(): AgentSessionServices { + return this.runtimeServices; + } + + get session(): AgentSession { + return this.currentSession; + } + + get cwd(): string { + return this.runtimeServices.cwd; + } + + get diagnostics(): readonly AgentSessionRuntimeDiagnostic[] { + return this.runtimeDiagnostics; + } + + get modelFallbackMessage(): string | undefined { + return this.fallbackMessage; + } + + setRebindSession(rebindSession?: (session: AgentSession) => Promise): void { + this.rebindSession = rebindSession; + } + + /** + * Set a synchronous callback that runs after `session_shutdown` handlers finish + * but before the current session is invalidated. + * + * This is for host-owned UI teardown that must not yield to the event loop, + * such as detaching extension-provided TUI components before the old extension + * context becomes stale. + */ + setBeforeSessionInvalidate(beforeSessionInvalidate?: () => void): void { + this.beforeSessionInvalidate = beforeSessionInvalidate; + } + + private async emitBeforeSwitch( + reason: "new" | "resume", + targetSessionFile?: string, + ): Promise<{ cancelled: boolean }> { + const runner = this.currentSession.extensionRunner; + if (!runner.hasHandlers("session_before_switch")) { + return { cancelled: false }; + } + + const result = await runner.emit({ + type: "session_before_switch", + reason, + targetSessionFile, + }); + return { cancelled: result?.cancel === true }; + } + + private async emitBeforeFork( + entryId: string, + options: { position: "before" | "at" }, + ): Promise<{ cancelled: boolean }> { + const runner = this.currentSession.extensionRunner; + if (!runner.hasHandlers("session_before_fork")) { + return { cancelled: false }; + } + + const result = await runner.emit({ + type: "session_before_fork", + entryId, + ...options, + }); + return { cancelled: result?.cancel === true }; + } + + private async teardownCurrent( + reason: SessionShutdownEvent["reason"], + targetSessionFile?: string, + ): Promise { + await emitSessionShutdownEvent(this.currentSession.extensionRunner, { + type: "session_shutdown", + reason, + targetSessionFile, + }); + this.beforeSessionInvalidate?.(); + this.currentSession.dispose(); + } + + private apply(result: CreateAgentSessionRuntimeResult): void { + this.currentSession = result.session; + this.runtimeServices = result.services; + this.runtimeDiagnostics = result.diagnostics; + this.fallbackMessage = result.modelFallbackMessage; + } + + private async finishSessionReplacement( + withSession?: (ctx: ReplacedSessionContext) => Promise, + ): Promise { + if (this.rebindSession) { + await this.rebindSession(this.currentSession); + } + if (withSession) { + await withSession(this.currentSession.createReplacedSessionContext()); + } + } + + async switchSession( + sessionPath: string, + options?: { + cwdOverride?: string; + withSession?: (ctx: ReplacedSessionContext) => Promise; + }, + ): Promise<{ cancelled: boolean }> { + const beforeResult = await this.emitBeforeSwitch("resume", sessionPath); + if (beforeResult.cancelled) { + return beforeResult; + } + + const previousSessionFile = this.currentSession.sessionFile; + const sessionManager = SessionManager.open(sessionPath, undefined, options?.cwdOverride); + assertSessionCwdExists(sessionManager, this.cwd); + await this.teardownCurrent("resume", sessionManager.getSessionFile()); + this.apply( + await this.createRuntime({ + cwd: sessionManager.getCwd(), + agentDir: this.runtimeServices.agentDir, + sessionManager, + sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile }, + }), + ); + await this.finishSessionReplacement(options?.withSession); + return { cancelled: false }; + } + + async newSession(options?: { + parentSession?: string; + setup?: (sessionManager: SessionManager) => Promise; + withSession?: (ctx: ReplacedSessionContext) => Promise; + }): Promise<{ cancelled: boolean }> { + const beforeResult = await this.emitBeforeSwitch("new"); + if (beforeResult.cancelled) { + return beforeResult; + } + + const previousSessionFile = this.currentSession.sessionFile; + const sessionDir = this.currentSession.sessionManager.getSessionDir(); + const sessionManager = SessionManager.create(this.cwd, sessionDir); + if (options?.parentSession) { + sessionManager.newSession({ parentSession: options.parentSession }); + } + + await this.teardownCurrent("new", sessionManager.getSessionFile()); + this.apply( + await this.createRuntime({ + cwd: this.cwd, + agentDir: this.runtimeServices.agentDir, + sessionManager, + sessionStartEvent: { type: "session_start", reason: "new", previousSessionFile }, + }), + ); + if (options?.setup) { + await options.setup(this.currentSession.sessionManager); + this.currentSession.agent.state.messages = + this.currentSession.sessionManager.buildSessionContext().messages; + } + await this.finishSessionReplacement(options?.withSession); + return { cancelled: false }; + } + + async fork( + entryId: string, + options?: { + position?: "before" | "at"; + withSession?: (ctx: ReplacedSessionContext) => Promise; + }, + ): Promise<{ cancelled: boolean; selectedText?: string }> { + const position = options?.position ?? "before"; + const beforeResult = await this.emitBeforeFork(entryId, { position }); + if (beforeResult.cancelled) { + return { cancelled: true }; + } + let targetLeafId: string | null; + let selectedText: string | undefined; + + const selectedEntry = this.currentSession.sessionManager.getEntry(entryId); + if (!selectedEntry) { + throw new Error("Invalid entry ID for forking"); + } + + if (position === "at") { + targetLeafId = selectedEntry.id; + } else { + if (selectedEntry.type !== "message" || selectedEntry.message.role !== "user") { + throw new Error("Invalid entry ID for forking"); + } + targetLeafId = selectedEntry.parentId; + selectedText = extractUserMessageText(selectedEntry.message.content); + } + + const previousSessionFile = this.currentSession.sessionFile; + if (this.currentSession.sessionManager.isPersisted()) { + const currentSessionFile = this.currentSession.sessionFile; + if (!currentSessionFile) { + throw new Error("Persisted session is missing a session file"); + } + const sessionDir = this.currentSession.sessionManager.getSessionDir(); + if (!targetLeafId) { + const sessionManager = SessionManager.create(this.cwd, sessionDir); + sessionManager.newSession({ parentSession: currentSessionFile }); + await this.teardownCurrent("fork", sessionManager.getSessionFile()); + this.apply( + await this.createRuntime({ + cwd: this.cwd, + agentDir: this.runtimeServices.agentDir, + sessionManager, + sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile }, + }), + ); + await this.finishSessionReplacement(options?.withSession); + return { cancelled: false, selectedText }; + } + + const sessionManager = SessionManager.open(currentSessionFile, sessionDir); + const forkedSessionPath = sessionManager.createBranchedSession(targetLeafId); + if (!forkedSessionPath) { + throw new Error("Failed to create forked session"); + } + await this.teardownCurrent("fork", sessionManager.getSessionFile()); + this.apply( + await this.createRuntime({ + cwd: sessionManager.getCwd(), + agentDir: this.runtimeServices.agentDir, + sessionManager, + sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile }, + }), + ); + await this.finishSessionReplacement(options?.withSession); + return { cancelled: false, selectedText }; + } + + const sessionManager = this.currentSession.sessionManager; + if (!targetLeafId) { + sessionManager.newSession({ parentSession: this.currentSession.sessionFile }); + } else { + sessionManager.createBranchedSession(targetLeafId); + } + await this.teardownCurrent("fork", sessionManager.getSessionFile()); + this.apply( + await this.createRuntime({ + cwd: this.cwd, + agentDir: this.runtimeServices.agentDir, + sessionManager, + sessionStartEvent: { type: "session_start", reason: "fork", previousSessionFile }, + }), + ); + await this.finishSessionReplacement(options?.withSession); + return { cancelled: false, selectedText }; + } + + /** + * Import a session JSONL file and switch runtime state to the imported session. + * + * @returns `{ cancelled: true }` when cancelled by `session_before_switch`, otherwise `{ cancelled: false }`. + * @throws {SessionImportFileNotFoundError} When the input path does not exist. + * @throws {MissingSessionCwdError} When the imported session cwd cannot be resolved and no override is provided. + */ + async importFromJsonl(inputPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> { + const resolvedPath = resolve(inputPath); + if (!existsSync(resolvedPath)) { + throw new SessionImportFileNotFoundError(resolvedPath); + } + + const sessionDir = this.currentSession.sessionManager.getSessionDir(); + if (!existsSync(sessionDir)) { + mkdirSync(sessionDir, { recursive: true }); + } + + const destinationPath = join(sessionDir, basename(resolvedPath)); + const beforeResult = await this.emitBeforeSwitch("resume", destinationPath); + if (beforeResult.cancelled) { + return beforeResult; + } + + const previousSessionFile = this.currentSession.sessionFile; + if (resolve(destinationPath) !== resolvedPath) { + copyFileSync(resolvedPath, destinationPath); + } + + const sessionManager = SessionManager.open(destinationPath, sessionDir, cwdOverride); + assertSessionCwdExists(sessionManager, this.cwd); + await this.teardownCurrent("resume", sessionManager.getSessionFile()); + this.apply( + await this.createRuntime({ + cwd: sessionManager.getCwd(), + agentDir: this.runtimeServices.agentDir, + sessionManager, + sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile }, + }), + ); + await this.finishSessionReplacement(); + return { cancelled: false }; + } + + async dispose(): Promise { + await emitSessionShutdownEvent(this.currentSession.extensionRunner, { + type: "session_shutdown", + reason: "quit", + }); + this.beforeSessionInvalidate?.(); + this.currentSession.dispose(); + } +} + +/** + * Create the initial runtime from a runtime factory and initial session target. + * + * The same factory is stored on the returned AgentSessionRuntime and reused for + * later /new, /resume, /fork, and import flows. + */ +export async function createAgentSessionRuntime( + createRuntime: CreateAgentSessionRuntimeFactory, + options: { + cwd: string; + agentDir: string; + sessionManager: SessionManager; + sessionStartEvent?: SessionStartEvent; + }, +): Promise { + assertSessionCwdExists(options.sessionManager, options.cwd); + const result = await createRuntime(options); + return new AgentSessionRuntime( + result.session, + result.services, + createRuntime, + result.diagnostics, + result.modelFallbackMessage, + ); +} + +export { + type AgentSessionRuntimeDiagnostic, + type AgentSessionServices, + type CreateAgentSessionFromServicesOptions, + type CreateAgentSessionServicesOptions, + createAgentSessionFromServices, + createAgentSessionServices, +} from "./agent-session-services.js"; diff --git a/src/agents/sessions/agent-session-services.ts b/src/agents/sessions/agent-session-services.ts new file mode 100644 index 00000000000..de692b6352f --- /dev/null +++ b/src/agents/sessions/agent-session-services.ts @@ -0,0 +1,211 @@ +import { join } from "node:path"; +import type { Model } from "openclaw/plugin-sdk/llm"; +import { getAgentDir } from "../config.js"; +import type { ThinkingLevel } from "../runtime/index.js"; +import { AuthStorage } from "./auth-storage.js"; +import type { SessionStartEvent, ToolDefinition } from "./extensions/index.js"; +import { ModelRegistry } from "./model-registry.js"; +import { + DefaultResourceLoader, + type DefaultResourceLoaderOptions, + type ResourceLoader, +} from "./resource-loader.js"; +import { + type CreateAgentSessionOptions, + type CreateAgentSessionResult, + createAgentSession, +} from "./sdk.js"; +import type { SessionManager } from "./session-manager.js"; +import { SettingsManager } from "./settings-manager.js"; + +/** + * Non-fatal issues collected while creating services or sessions. + * + * Runtime creation returns diagnostics to the caller instead of printing or + * exiting. The app layer decides whether warnings should be shown and whether + * errors should abort startup. + */ +export interface AgentSessionRuntimeDiagnostic { + type: "info" | "warning" | "error"; + message: string; +} + +/** + * Inputs for creating cwd-bound runtime services. + * + * These services are recreated whenever the effective session cwd changes. + * CLI-provided resource paths should be resolved to absolute paths before they + * reach this function, so later cwd switches do not reinterpret them. + */ +export interface CreateAgentSessionServicesOptions { + cwd: string; + agentDir?: string; + authStorage?: AuthStorage; + settingsManager?: SettingsManager; + modelRegistry?: ModelRegistry; + extensionFlagValues?: Map; + resourceLoaderOptions?: Omit< + DefaultResourceLoaderOptions, + "cwd" | "agentDir" | "settingsManager" + >; +} + +/** + * Inputs for creating an AgentSession from already-created services. + * + * Use this after services exist and any cwd-bound model/tool/session options + * have been resolved against those services. + */ +export interface CreateAgentSessionFromServicesOptions { + services: AgentSessionServices; + sessionManager: SessionManager; + sessionStartEvent?: SessionStartEvent; + model?: Model; + thinkingLevel?: ThinkingLevel; + scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>; + tools?: string[]; + noTools?: CreateAgentSessionOptions["noTools"]; + customTools?: ToolDefinition[]; +} + +/** + * Coherent cwd-bound runtime services for one effective session cwd. + * + * This is infrastructure only. The AgentSession itself is created separately so + * session options can be resolved against these services first. + */ +export interface AgentSessionServices { + cwd: string; + agentDir: string; + authStorage: AuthStorage; + settingsManager: SettingsManager; + modelRegistry: ModelRegistry; + resourceLoader: ResourceLoader; + diagnostics: AgentSessionRuntimeDiagnostic[]; +} + +function applyExtensionFlagValues( + resourceLoader: ResourceLoader, + extensionFlagValues: Map | undefined, +): AgentSessionRuntimeDiagnostic[] { + if (!extensionFlagValues) { + return []; + } + + const diagnostics: AgentSessionRuntimeDiagnostic[] = []; + const extensionsResult = resourceLoader.getExtensions(); + const registeredFlags = new Map(); + for (const extension of extensionsResult.extensions) { + for (const [name, flag] of extension.flags) { + registeredFlags.set(name, { type: flag.type }); + } + } + + const unknownFlags: string[] = []; + for (const [name, value] of extensionFlagValues) { + const flag = registeredFlags.get(name); + if (!flag) { + unknownFlags.push(name); + continue; + } + if (flag.type === "boolean") { + extensionsResult.runtime.flagValues.set(name, true); + continue; + } + if (typeof value === "string") { + extensionsResult.runtime.flagValues.set(name, value); + continue; + } + diagnostics.push({ + type: "error", + message: `Extension flag "--${name}" requires a value`, + }); + } + + if (unknownFlags.length > 0) { + diagnostics.push({ + type: "error", + message: `Unknown option${unknownFlags.length === 1 ? "" : "s"}: ${unknownFlags.map((name) => `--${name}`).join(", ")}`, + }); + } + + return diagnostics; +} + +/** + * Create cwd-bound runtime services. + * + * Returns services plus diagnostics. It does not create an AgentSession. + */ +export async function createAgentSessionServices( + options: CreateAgentSessionServicesOptions, +): Promise { + const cwd = options.cwd; + const agentDir = options.agentDir ?? getAgentDir(); + const authStorage = options.authStorage ?? AuthStorage.create(join(agentDir, "auth.json")); + const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir); + const modelRegistry = + options.modelRegistry ?? ModelRegistry.create(authStorage, join(agentDir, "models.json")); + const resourceLoader = new DefaultResourceLoader({ + ...options.resourceLoaderOptions, + cwd, + agentDir, + settingsManager, + }); + await resourceLoader.reload(); + + const diagnostics: AgentSessionRuntimeDiagnostic[] = []; + const extensionsResult = resourceLoader.getExtensions(); + for (const { name, config, extensionPath } of extensionsResult.runtime + .pendingProviderRegistrations) { + try { + modelRegistry.registerProvider(name, config); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + diagnostics.push({ + type: "error", + message: `Extension "${extensionPath}" error: ${message}`, + }); + } + } + extensionsResult.runtime.pendingProviderRegistrations = []; + diagnostics.push(...applyExtensionFlagValues(resourceLoader, options.extensionFlagValues)); + + return { + cwd, + agentDir, + authStorage, + settingsManager, + modelRegistry, + resourceLoader, + diagnostics, + }; +} + +/** + * Create an AgentSession from previously created services. + * + * This keeps session creation separate from service creation so callers can + * resolve model, thinking, tools, and other session inputs against the target + * cwd before constructing the session. + */ +export async function createAgentSessionFromServices( + options: CreateAgentSessionFromServicesOptions, +): Promise { + return createAgentSession({ + cwd: options.services.cwd, + agentDir: options.services.agentDir, + authStorage: options.services.authStorage, + settingsManager: options.services.settingsManager, + modelRegistry: options.services.modelRegistry, + resourceLoader: options.services.resourceLoader, + sessionManager: options.sessionManager, + model: options.model, + thinkingLevel: options.thinkingLevel, + scopedModels: options.scopedModels, + tools: options.tools, + noTools: options.noTools, + customTools: options.customTools, + sessionStartEvent: options.sessionStartEvent, + }); +} diff --git a/src/agents/sessions/agent-session.ts b/src/agents/sessions/agent-session.ts new file mode 100644 index 00000000000..95e1aebc63d --- /dev/null +++ b/src/agents/sessions/agent-session.ts @@ -0,0 +1,3222 @@ +/** + * AgentSession - Core abstraction for agent lifecycle and session management. + * + * This class is shared between all run modes (interactive, print, rpc). + * It encapsulates: + * - Agent state access + * - Event subscription with automatic session persistence + * - Model and thinking level management + * - Compaction (manual and auto) + * - Bash execution + * - Session switching and branching + * + * Modes use this class and add their own I/O layer on top. + */ + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { basename, dirname, resolve } from "node:path"; +import type { + AssistantMessage, + ImageContent, + Message, + Model, + TextContent, +} from "openclaw/plugin-sdk/llm"; +import { + clampThinkingLevel, + cleanupSessionResources, + getSupportedThinkingLevels, + isContextOverflow, + modelsAreEqual, + resetApiProviders, + streamSimple, +} from "openclaw/plugin-sdk/llm"; +import type { + Agent, + AgentEvent, + AgentMessage, + AgentState, + AgentTool, + ThinkingLevel, +} from "../runtime/index.js"; +import { stripFrontmatter } from "../utils/frontmatter.js"; +import { sleep } from "../utils/sleep.js"; +import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage } from "./auth-guidance.js"; +import { type BashResult, executeBashWithOperations } from "./bash-executor.js"; +import { + type CompactionResult, + calculateContextTokens, + collectEntriesForBranchSummary, + compact, + estimateContextTokens, + generateBranchSummary, + prepareCompaction, + shouldCompact, +} from "./compaction/index.js"; +import { DEFAULT_THINKING_LEVEL } from "./defaults.js"; +import { + type ContextUsage, + type ExtensionCommandContextActions, + type ExtensionErrorListener, + ExtensionRunner, + type ExtensionUIContext, + type InputSource, + type MessageEndEvent, + type MessageStartEvent, + type MessageUpdateEvent, + type ReplacedSessionContext, + type SessionStartEvent, + type ShutdownHandler, + type ToolDefinition, + type ToolExecutionEndEvent, + type ToolExecutionStartEvent, + type ToolExecutionUpdateEvent, + type ToolInfo, + type TreePreparation, + type TurnEndEvent, + type TurnStartEvent, + wrapRegisteredTools, +} from "./extensions/index.js"; +import { emitSessionShutdownEvent } from "./extensions/runner.js"; +import type { BashExecutionMessage, CustomMessage } from "./messages.js"; +import type { ModelRegistry } from "./model-registry.js"; +import { expandPromptTemplate, type PromptTemplate } from "./prompt-templates.js"; +import type { ResourceExtensionPaths, ResourceLoader } from "./resource-loader.js"; +import type { BranchSummaryEntry, CompactionEntry, SessionManager } from "./session-manager.js"; +import { + CURRENT_SESSION_VERSION, + getLatestCompactionEntry, + type SessionHeader, +} from "./session-manager.js"; +import type { SettingsManager } from "./settings-manager.js"; +import type { SlashCommandInfo } from "./slash-commands.js"; +import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js"; +import { type BuildSystemPromptOptions, buildSystemPrompt } from "./system-prompt.js"; +import { type BashOperations, createLocalBashOperations } from "./tools/bash.js"; +import { createAllToolDefinitions } from "./tools/index.js"; +import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js"; + +// ============================================================================ +// Skill Block Parsing +// ============================================================================ + +/** Parsed skill block from a user message */ +export interface ParsedSkillBlock { + name: string; + location: string; + content: string; + userMessage: string | undefined; +} + +/** + * Parse a skill block from message text. + * Returns null if the text doesn't contain a skill block. + */ +export function parseSkillBlock(text: string): ParsedSkillBlock | null { + const match = text.match( + /^\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/, + ); + if (!match) { + return null; + } + return { + name: match[1], + location: match[2], + content: match[3], + userMessage: match[4]?.trim() || undefined, + }; +} + +/** Session-specific events that extend the core AgentEvent */ +export type AgentSessionEvent = + | Exclude + | { + type: "agent_end"; + messages: AgentMessage[]; + willRetry: boolean; + } + | { + type: "queue_update"; + steering: readonly string[]; + followUp: readonly string[]; + } + | { type: "compaction_start"; reason: "manual" | "threshold" | "overflow" } + | { type: "session_info_changed"; name: string | undefined } + | { type: "thinking_level_changed"; level: ThinkingLevel } + | { + type: "compaction_end"; + reason: "manual" | "threshold" | "overflow"; + result: CompactionResult | undefined; + aborted: boolean; + willRetry: boolean; + errorMessage?: string; + } + | { + type: "auto_retry_start"; + attempt: number; + maxAttempts: number; + delayMs: number; + errorMessage: string; + } + | { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }; + +/** Listener function for agent session events */ +export type AgentSessionEventListener = (event: AgentSessionEvent) => void; + +// ============================================================================ +// Types +// ============================================================================ + +export interface AgentSessionConfig { + agent: Agent; + sessionManager: SessionManager; + settingsManager: SettingsManager; + cwd: string; + /** Models to cycle through with Ctrl+P (from --models flag) */ + scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>; + /** Resource loader for skills, prompts, themes, context files, system prompt */ + resourceLoader: ResourceLoader; + /** SDK custom tools registered outside extensions */ + customTools?: ToolDefinition[]; + /** Model registry for API key resolution and model discovery */ + modelRegistry: ModelRegistry; + /** Initial active built-in tool names. Default: [read, bash, edit, write] */ + initialActiveToolNames?: string[]; + /** Optional allowlist of tool names. When provided, only these tool names are exposed. */ + allowedToolNames?: string[]; + /** + * Override base tools (useful for custom runtimes). + * + * These are synthesized into minimal ToolDefinitions internally so AgentSession can keep + * a definition-first registry even when callers provide plain AgentTool instances. + */ + baseToolsOverride?: Record; + /** Mutable ref used by Agent to access the current ExtensionRunner */ + extensionRunnerRef?: { current?: ExtensionRunner }; + /** Session start event metadata emitted when extensions bind to this runtime. */ + sessionStartEvent?: SessionStartEvent; +} + +export interface ExtensionBindings { + uiContext?: ExtensionUIContext; + commandContextActions?: ExtensionCommandContextActions; + abortHandler?: () => void; + shutdownHandler?: ShutdownHandler; + onError?: ExtensionErrorListener; +} + +/** Options for AgentSession.prompt() */ +export interface PromptOptions { + /** Whether to expand file-based prompt templates (default: true) */ + expandPromptTemplates?: boolean; + /** Image attachments */ + images?: ImageContent[]; + /** When streaming, how to queue the message: "steer" (interrupt) or "followUp" (wait). Required if streaming. */ + streamingBehavior?: "steer" | "followUp"; + /** Source of input for extension input event handlers. Defaults to "interactive". */ + source?: InputSource; + /** Internal hook used by RPC mode to observe prompt preflight acceptance or rejection. */ + preflightResult?: (success: boolean) => void; +} + +/** Result from cycleModel() */ +export interface ModelCycleResult { + model: Model; + thinkingLevel: ThinkingLevel; + /** Whether cycling through scoped models (--models flag) or all available */ + isScoped: boolean; +} + +/** Session statistics for /session command */ +export interface SessionStats { + sessionFile: string | undefined; + sessionId: string; + userMessages: number; + assistantMessages: number; + toolCalls: number; + toolResults: number; + totalMessages: number; + tokens: { + input: number; + output: number; + cacheRead: number; + cacheWrite: number; + total: number; + }; + cost: number; + contextUsage?: ContextUsage; +} + +interface ToolDefinitionEntry { + definition: ToolDefinition; + sourceInfo: SourceInfo; +} + +// ============================================================================ +// Constants +// ============================================================================ + +/** Standard thinking levels */ +const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high"]; + +// ============================================================================ +// AgentSession Class +// ============================================================================ + +export class AgentSession { + readonly agent: Agent; + readonly sessionManager: SessionManager; + readonly settingsManager: SettingsManager; + + private scopedModelEntries: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>; + + // Event subscription state + private unsubscribeAgent?: () => void; + private eventListeners: AgentSessionEventListener[] = []; + + /** Tracks pending steering messages for UI display. Removed when delivered. */ + private steeringMessages: string[] = []; + /** Tracks pending follow-up messages for UI display. Removed when delivered. */ + private followUpMessages: string[] = []; + /** Messages queued to be included with the next user prompt as context ("asides"). */ + private pendingNextTurnMessages: CustomMessage[] = []; + + // Compaction state + private compactionAbortController: AbortController | undefined = undefined; + private autoCompactionAbortController: AbortController | undefined = undefined; + private overflowRecoveryAttempted = false; + + // Branch summarization state + private branchSummaryAbortController: AbortController | undefined = undefined; + + // Retry state + private retryAbortController: AbortController | undefined = undefined; + private retryCount = 0; + + // Bash execution state + private bashAbortController: AbortController | undefined = undefined; + private pendingBashMessages: BashExecutionMessage[] = []; + + // Extension system + private currentExtensionRunner!: ExtensionRunner; + private turnIndex = 0; + + private sessionResourceLoader: ResourceLoader; + private customTools: ToolDefinition[]; + private baseToolDefinitions: Map = new Map(); + private cwd: string; + private extensionRunnerRef?: { current?: ExtensionRunner }; + private initialActiveToolNames?: string[]; + private allowedToolNames?: Set; + private baseToolsOverride?: Record; + private sessionStartEvent: SessionStartEvent; + private extensionUIContext?: ExtensionUIContext; + private extensionCommandContextActions?: ExtensionCommandContextActions; + private extensionAbortHandler?: () => void; + private extensionShutdownHandler?: ShutdownHandler; + private extensionErrorListener?: ExtensionErrorListener; + private extensionErrorUnsubscriber?: () => void; + + // Model registry for API key resolution + private sessionModelRegistry: ModelRegistry; + + // Tool registry for extension getTools/setTools + private toolRegistry: Map = new Map(); + private toolDefinitions: Map = new Map(); + private toolPromptSnippets: Map = new Map(); + private toolPromptGuidelines: Map = new Map(); + + // Base system prompt (without extension appends) - used to apply fresh appends each turn + private baseSystemPrompt = ""; + private baseSystemPromptOptions!: BuildSystemPromptOptions; + + constructor(config: AgentSessionConfig) { + this.agent = config.agent; + this.sessionManager = config.sessionManager; + this.settingsManager = config.settingsManager; + this.scopedModelEntries = config.scopedModels ?? []; + this.sessionResourceLoader = config.resourceLoader; + this.customTools = config.customTools ?? []; + this.cwd = config.cwd; + this.sessionModelRegistry = config.modelRegistry; + this.extensionRunnerRef = config.extensionRunnerRef; + this.initialActiveToolNames = config.initialActiveToolNames; + this.allowedToolNames = config.allowedToolNames ? new Set(config.allowedToolNames) : undefined; + this.baseToolsOverride = config.baseToolsOverride; + this.sessionStartEvent = config.sessionStartEvent ?? { + type: "session_start", + reason: "startup", + }; + + // Always subscribe to agent events for internal handling + // (session persistence, extensions, auto-compaction, retry logic) + this.unsubscribeAgent = this.agent.subscribe(this.handleAgentEvent); + this.installAgentToolHooks(); + + this.buildRuntime({ + activeToolNames: this.initialActiveToolNames, + includeAllExtensionTools: true, + }); + } + + /** Model registry for API key resolution and model discovery */ + get modelRegistry(): ModelRegistry { + return this.sessionModelRegistry; + } + + private async getRequiredRequestAuth(model: Model): Promise<{ + apiKey: string; + headers?: Record; + }> { + const result = await this.sessionModelRegistry.getApiKeyAndHeaders(model); + if (!result.ok) { + if (result.error.startsWith("No API key found")) { + throw new Error(formatNoApiKeyFoundMessage(model.provider)); + } + throw new Error(result.error); + } + if (result.apiKey) { + return { apiKey: result.apiKey, headers: result.headers }; + } + + const isOAuth = this.sessionModelRegistry.isUsingOAuth(model); + if (isOAuth) { + throw new Error( + `Authentication failed for "${model.provider}". ` + + `Credentials may have expired or network is unavailable. ` + + `Run '/login ${model.provider}' to re-authenticate.`, + ); + } + throw new Error(formatNoApiKeyFoundMessage(model.provider)); + } + + private async getCompactionRequestAuth(model: Model): Promise<{ + apiKey?: string; + headers?: Record; + }> { + if (this.agent.streamFn === streamSimple) { + return this.getRequiredRequestAuth(model); + } + + const result = await this.sessionModelRegistry.getApiKeyAndHeaders(model); + return result.ok ? { apiKey: result.apiKey, headers: result.headers } : {}; + } + + /** + * Install tool hooks once on the Agent instance. + * + * The callbacks read `this.currentExtensionRunner` at execution time, so extension reload swaps in the + * new runner without reinstalling hooks. Extension-specific tool wrappers are still used to adapt + * registered tool execution to the extension context. Tool call and tool result interception now + * happens here instead of in wrappers. + */ + private installAgentToolHooks(): void { + this.agent.beforeToolCall = async ({ toolCall, args }) => { + const runner = this.currentExtensionRunner; + if (!runner.hasHandlers("tool_call")) { + return undefined; + } + + try { + return await runner.emitToolCall({ + type: "tool_call", + toolName: toolCall.name, + toolCallId: toolCall.id, + input: args as Record, + }); + } catch (err) { + if (err instanceof Error) { + throw err; + } + throw new Error(`Extension failed, blocking execution: ${String(err)}`, { cause: err }); + } + }; + + this.agent.afterToolCall = async ({ toolCall, args, result, isError }) => { + const runner = this.currentExtensionRunner; + if (!runner.hasHandlers("tool_result")) { + return undefined; + } + + const hookResult = await runner.emitToolResult({ + type: "tool_result", + toolName: toolCall.name, + toolCallId: toolCall.id, + input: args as Record, + content: result.content, + details: result.details, + isError, + }); + + if (!hookResult) { + return undefined; + } + + return { + content: hookResult.content, + details: hookResult.details, + isError: hookResult.isError ?? isError, + }; + }; + } + + // ========================================================================= + // Event Subscription + // ========================================================================= + + /** Emit an event to all listeners */ + private emit(event: AgentSessionEvent): void { + for (const l of this.eventListeners) { + l(event); + } + } + + private emitQueueUpdate(): void { + this.emit({ + type: "queue_update", + steering: [...this.steeringMessages], + followUp: [...this.followUpMessages], + }); + } + + // Track last assistant message for auto-compaction check + private lastAssistantMessage: AssistantMessage | undefined = undefined; + + /** Internal handler for agent events - shared by subscribe and reconnect */ + private handleAgentEvent = async (event: AgentEvent): Promise => { + // When a user message starts, check if it's from either queue and remove it BEFORE emitting + // This ensures the UI sees the updated queue state + if (event.type === "message_start" && event.message.role === "user") { + this.overflowRecoveryAttempted = false; + const messageText = this.getUserMessageText(event.message); + if (messageText) { + // Check steering queue first + const steeringIndex = this.steeringMessages.indexOf(messageText); + if (steeringIndex !== -1) { + this.steeringMessages.splice(steeringIndex, 1); + this.emitQueueUpdate(); + } else { + // Check follow-up queue + const followUpIndex = this.followUpMessages.indexOf(messageText); + if (followUpIndex !== -1) { + this.followUpMessages.splice(followUpIndex, 1); + this.emitQueueUpdate(); + } + } + } + } + + // Emit to extensions first + await this.emitExtensionEvent(event); + + // Notify all listeners + this.emit( + event.type === "agent_end" + ? { ...event, willRetry: this.willRetryAfterAgentEnd(event) } + : event, + ); + + // Handle session persistence + if (event.type === "message_end") { + // Check if this is a custom message from extensions + if (event.message.role === "custom") { + // Persist as CustomMessageEntry + this.sessionManager.appendCustomMessageEntry( + event.message.customType, + event.message.content, + event.message.display, + event.message.details, + ); + } else if ( + event.message.role === "user" || + event.message.role === "assistant" || + event.message.role === "toolResult" + ) { + // Regular LLM message - persist as SessionMessageEntry + this.sessionManager.appendMessage(event.message); + } + // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere + + // Track assistant message for auto-compaction (checked on agent_end) + if (event.message.role === "assistant") { + this.lastAssistantMessage = event.message; + + const assistantMsg = event.message; + if (assistantMsg.stopReason !== "error") { + this.overflowRecoveryAttempted = false; + } + + // Reset retry counter immediately on successful assistant response + // This prevents accumulation across multiple LLM calls within a turn + if (assistantMsg.stopReason !== "error" && this.retryCount > 0) { + this.emit({ + type: "auto_retry_end", + success: true, + attempt: this.retryCount, + }); + this.retryCount = 0; + } + } + } + }; + + private willRetryAfterAgentEnd(event: Extract): boolean { + const settings = this.settingsManager.getRetrySettings(); + if (!settings.enabled || this.retryCount >= settings.maxRetries) { + return false; + } + + for (let i = event.messages.length - 1; i >= 0; i--) { + const message = event.messages[i]; + if (message.role === "assistant") { + return this.isRetryableError(message); + } + } + return false; + } + + /** Extract text content from a message */ + private getUserMessageText(message: Message): string { + if (message.role !== "user") { + return ""; + } + const content = message.content; + if (typeof content === "string") { + return content; + } + const textBlocks = content.filter((c) => c.type === "text"); + return textBlocks.map((c) => c.text).join(""); + } + + /** Find the last assistant message in agent state (including aborted ones) */ + private findLastAssistantMessage(): AssistantMessage | undefined { + const messages = this.agent.state.messages; + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i]; + if (msg.role === "assistant") { + return msg; + } + } + return undefined; + } + + private replaceMessageInPlace(target: AgentMessage, replacement: AgentMessage): void { + // Agent-core stores the finalized message object in its state before emitting message_end. + // SessionManager persistence happens later in handleAgentEvent() with event.message. + // Mutating this object in place keeps agent state, later turn/agent events, listeners, + // and the eventual SessionManager.appendMessage(event.message) persistence in sync. + if (target === replacement) { + return; + } + + const targetRecord = target as unknown as Record; + for (const key of Object.keys(targetRecord)) { + delete targetRecord[key]; + } + Object.assign(targetRecord, replacement); + } + + /** Emit extension events based on agent events */ + private async emitExtensionEvent(event: AgentEvent): Promise { + if (event.type === "agent_start") { + this.turnIndex = 0; + await this.currentExtensionRunner.emit({ type: "agent_start" }); + } else if (event.type === "agent_end") { + await this.currentExtensionRunner.emit({ type: "agent_end", messages: event.messages }); + } else if (event.type === "turn_start") { + const extensionEvent: TurnStartEvent = { + type: "turn_start", + turnIndex: this.turnIndex, + timestamp: Date.now(), + }; + await this.currentExtensionRunner.emit(extensionEvent); + } else if (event.type === "turn_end") { + const extensionEvent: TurnEndEvent = { + type: "turn_end", + turnIndex: this.turnIndex, + message: event.message, + toolResults: event.toolResults, + }; + await this.currentExtensionRunner.emit(extensionEvent); + this.turnIndex++; + } else if (event.type === "message_start") { + const extensionEvent: MessageStartEvent = { + type: "message_start", + message: event.message, + }; + await this.currentExtensionRunner.emit(extensionEvent); + } else if (event.type === "message_update") { + const extensionEvent: MessageUpdateEvent = { + type: "message_update", + message: event.message, + assistantMessageEvent: event.assistantMessageEvent, + }; + await this.currentExtensionRunner.emit(extensionEvent); + } else if (event.type === "message_end") { + const extensionEvent: MessageEndEvent = { + type: "message_end", + message: event.message, + }; + const replacement = await this.currentExtensionRunner.emitMessageEnd(extensionEvent); + if (replacement) { + this.replaceMessageInPlace(event.message, replacement); + } + } else if (event.type === "tool_execution_start") { + const extensionEvent: ToolExecutionStartEvent = { + type: "tool_execution_start", + toolCallId: event.toolCallId, + toolName: event.toolName, + args: event.args, + }; + await this.currentExtensionRunner.emit(extensionEvent); + } else if (event.type === "tool_execution_update") { + const extensionEvent: ToolExecutionUpdateEvent = { + type: "tool_execution_update", + toolCallId: event.toolCallId, + toolName: event.toolName, + args: event.args, + partialResult: event.partialResult, + }; + await this.currentExtensionRunner.emit(extensionEvent); + } else if (event.type === "tool_execution_end") { + const extensionEvent: ToolExecutionEndEvent = { + type: "tool_execution_end", + toolCallId: event.toolCallId, + toolName: event.toolName, + result: event.result, + isError: event.isError, + }; + await this.currentExtensionRunner.emit(extensionEvent); + } + } + + /** + * Subscribe to agent events. + * Session persistence is handled internally (saves messages on message_end). + * Multiple listeners can be added. Returns unsubscribe function for this listener. + */ + subscribe(listener: AgentSessionEventListener): () => void { + this.eventListeners.push(listener); + + // Return unsubscribe function for this specific listener + return () => { + const index = this.eventListeners.indexOf(listener); + if (index !== -1) { + this.eventListeners.splice(index, 1); + } + }; + } + + /** + * Temporarily disconnect from agent events. + * User listeners are preserved and will receive events again after resubscribe(). + * Used internally during operations that need to pause event processing. + */ + private disconnectFromAgent(): void { + if (this.unsubscribeAgent) { + this.unsubscribeAgent(); + this.unsubscribeAgent = undefined; + } + } + + /** + * Reconnect to agent events after disconnectFromAgent(). + * Preserves all existing listeners. + */ + private reconnectToAgent(): void { + if (this.unsubscribeAgent) { + return; + } // Already connected + this.unsubscribeAgent = this.agent.subscribe(this.handleAgentEvent); + } + + /** + * Remove all listeners and disconnect from agent. + * Call this when completely done with the session. + */ + dispose(): void { + this.currentExtensionRunner.invalidate( + "This extension ctx is stale after session replacement or reload. Do not use a captured api or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().", + ); + this.disconnectFromAgent(); + this.eventListeners = []; + cleanupSessionResources(this.sessionId); + } + + // ========================================================================= + // Read-only State Access + // ========================================================================= + + /** Full agent state */ + get state(): AgentState { + return this.agent.state; + } + + /** Current model (may be undefined if not yet selected) */ + get model(): Model | undefined { + return this.agent.state.model; + } + + /** Current thinking level */ + get thinkingLevel(): ThinkingLevel { + return this.agent.state.thinkingLevel; + } + + /** Whether agent is currently streaming a response */ + get isStreaming(): boolean { + return this.agent.state.isStreaming; + } + + /** Current effective system prompt (includes any per-turn extension modifications) */ + get systemPrompt(): string { + return this.agent.state.systemPrompt; + } + + /** Current retry attempt (0 if not retrying) */ + get retryAttempt(): number { + return this.retryCount; + } + + /** + * Get the names of currently active tools. + * Returns the names of tools currently set on the agent. + */ + getActiveToolNames(): string[] { + return this.agent.state.tools.map((t) => t.name); + } + + /** + * Get all configured tools with name, description, parameter schema, and source metadata. + */ + getAllTools(): ToolInfo[] { + return Array.from(this.toolDefinitions.values()).map(({ definition, sourceInfo }) => ({ + name: definition.name, + description: definition.description, + parameters: definition.parameters, + sourceInfo, + })); + } + + getToolDefinition(name: string): ToolDefinition | undefined { + return this.toolDefinitions.get(name)?.definition; + } + + /** + * Set active tools by name. + * Only tools in the registry can be enabled. Unknown tool names are ignored. + * Also rebuilds the system prompt to reflect the new tool set. + * Changes take effect on the next agent turn. + */ + setActiveToolsByName(toolNames: string[]): void { + const tools: AgentTool[] = []; + const validToolNames: string[] = []; + for (const name of toolNames) { + const tool = this.toolRegistry.get(name); + if (tool) { + tools.push(tool); + validToolNames.push(name); + } + } + this.agent.state.tools = tools; + + // Rebuild base system prompt with new tool set + this.baseSystemPrompt = this.rebuildSystemPrompt(validToolNames); + this.agent.state.systemPrompt = this.baseSystemPrompt; + } + + /** Whether compaction or branch summarization is currently running */ + get isCompacting(): boolean { + return ( + this.autoCompactionAbortController !== undefined || + this.compactionAbortController !== undefined || + this.branchSummaryAbortController !== undefined + ); + } + + /** All messages including custom types like BashExecutionMessage */ + get messages(): AgentMessage[] { + return this.agent.state.messages; + } + + /** Current steering mode */ + get steeringMode(): "all" | "one-at-a-time" { + return this.agent.steeringMode; + } + + /** Current follow-up mode */ + get followUpMode(): "all" | "one-at-a-time" { + return this.agent.followUpMode; + } + + /** Current session file path, or undefined if sessions are disabled */ + get sessionFile(): string | undefined { + return this.sessionManager.getSessionFile(); + } + + /** Current session ID */ + get sessionId(): string { + return this.sessionManager.getSessionId(); + } + + /** Current session display name, if set */ + get sessionName(): string | undefined { + return this.sessionManager.getSessionName(); + } + + /** Scoped models for cycling (from --models flag) */ + get scopedModels(): ReadonlyArray<{ model: Model; thinkingLevel?: ThinkingLevel }> { + return this.scopedModelEntries; + } + + /** Update scoped models for cycling */ + setScopedModels(scopedModels: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>): void { + this.scopedModelEntries = scopedModels; + } + + /** File-based prompt templates */ + get promptTemplates(): ReadonlyArray { + return this.sessionResourceLoader.getPrompts().prompts; + } + + private normalizePromptSnippet(text: string | undefined): string | undefined { + if (!text) { + return undefined; + } + const oneLine = text + .replace(/[\r\n]+/g, " ") + .replace(/\s+/g, " ") + .trim(); + return oneLine.length > 0 ? oneLine : undefined; + } + + private normalizePromptGuidelines(guidelines: string[] | undefined): string[] { + if (!guidelines || guidelines.length === 0) { + return []; + } + + const unique = new Set(); + for (const guideline of guidelines) { + const normalized = guideline.trim(); + if (normalized.length > 0) { + unique.add(normalized); + } + } + return Array.from(unique); + } + + private rebuildSystemPrompt(toolNames: string[]): string { + const validToolNames = toolNames.filter((name) => this.toolRegistry.has(name)); + const toolSnippets: Record = {}; + const promptGuidelines: string[] = []; + for (const name of validToolNames) { + const snippet = this.toolPromptSnippets.get(name); + if (snippet) { + toolSnippets[name] = snippet; + } + + const toolGuidelines = this.toolPromptGuidelines.get(name); + if (toolGuidelines) { + promptGuidelines.push(...toolGuidelines); + } + } + + const loaderSystemPrompt = this.sessionResourceLoader.getSystemPrompt(); + const loaderAppendSystemPrompt = this.sessionResourceLoader.getAppendSystemPrompt(); + const appendSystemPrompt = + loaderAppendSystemPrompt.length > 0 ? loaderAppendSystemPrompt.join("\n\n") : undefined; + const loadedSkills = this.sessionResourceLoader.getSkills().skills; + const loadedContextFiles = this.sessionResourceLoader.getAgentsFiles().agentsFiles; + + this.baseSystemPromptOptions = { + cwd: this.cwd, + skills: loadedSkills, + contextFiles: loadedContextFiles, + customPrompt: loaderSystemPrompt, + appendSystemPrompt, + selectedTools: validToolNames, + toolSnippets, + promptGuidelines, + }; + return buildSystemPrompt(this.baseSystemPromptOptions); + } + + // ========================================================================= + // Prompting + // ========================================================================= + + private async runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise { + try { + await this.agent.prompt(messages); + while (await this.handlePostAgentRun()) { + await this.agent.continue(); + } + } finally { + this.flushPendingBashMessages(); + } + } + + private async handlePostAgentRun(): Promise { + const msg = this.lastAssistantMessage; + this.lastAssistantMessage = undefined; + if (!msg) { + return false; + } + + if (this.isRetryableError(msg) && (await this.prepareRetry(msg))) { + return true; + } + + if (msg.stopReason === "error" && this.retryCount > 0) { + this.emit({ + type: "auto_retry_end", + success: false, + attempt: this.retryCount, + finalError: msg.errorMessage, + }); + this.retryCount = 0; + } + + return await this.checkCompaction(msg); + } + + /** + * Send a prompt to the agent. + * - Handles extension commands immediately, even during streaming + * - Expands file-based prompt templates by default + * - During streaming, queues via steer() or followUp() based on streamingBehavior option + * - Validates model and API key before sending (when not streaming) + * @throws Error if streaming and no streamingBehavior specified + * @throws Error if no model selected or no API key available (when not streaming) + */ + async prompt(text: string, options?: PromptOptions): Promise { + const expandPromptTemplates = options?.expandPromptTemplates ?? true; + const preflightResult = options?.preflightResult; + let messages: AgentMessage[] | undefined; + + try { + // Handle extension commands first (execute immediately, even during streaming) + // Extension commands manage their own LLM interaction via the session API. + if (expandPromptTemplates && text.startsWith("/")) { + const handled = await this.tryExecuteExtensionCommand(text); + if (handled) { + // Extension command executed, no prompt to send + preflightResult?.(true); + return; + } + } + + // Emit input event for extension interception (before skill/template expansion) + let currentText = text; + let currentImages = options?.images; + if (this.currentExtensionRunner.hasHandlers("input")) { + const inputResult = await this.currentExtensionRunner.emitInput( + currentText, + currentImages, + options?.source ?? "interactive", + ); + if (inputResult.action === "handled") { + preflightResult?.(true); + return; + } + if (inputResult.action === "transform") { + currentText = inputResult.text; + currentImages = inputResult.images ?? currentImages; + } + } + + // Expand skill commands (/skill:name args) and prompt templates (/template args) + let expandedText = currentText; + if (expandPromptTemplates) { + expandedText = this.expandSkillCommand(expandedText); + expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]); + } + + // If streaming, queue via steer() or followUp() based on option + if (this.isStreaming) { + if (!options?.streamingBehavior) { + throw new Error( + "Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.", + ); + } + if (options.streamingBehavior === "followUp") { + await this.queueFollowUp(expandedText, currentImages); + } else { + await this.queueSteer(expandedText, currentImages); + } + preflightResult?.(true); + return; + } + + // Flush any pending bash messages before the new prompt + this.flushPendingBashMessages(); + + // Validate model + if (!this.model) { + throw new Error(formatNoModelSelectedMessage()); + } + + if (!this.sessionModelRegistry.hasConfiguredAuth(this.model)) { + const isOAuth = this.sessionModelRegistry.isUsingOAuth(this.model); + if (isOAuth) { + throw new Error( + `Authentication failed for "${this.model.provider}". ` + + `Credentials may have expired or network is unavailable. ` + + `Run '/login ${this.model.provider}' to re-authenticate.`, + ); + } + throw new Error(formatNoApiKeyFoundMessage(this.model.provider)); + } + + // Check if we need to compact before sending (catches aborted responses) + const lastAssistant = this.findLastAssistantMessage(); + if (lastAssistant && (await this.checkCompaction(lastAssistant, false))) { + try { + await this.agent.continue(); + while (await this.handlePostAgentRun()) { + await this.agent.continue(); + } + } finally { + this.flushPendingBashMessages(); + } + } + + // Build messages array (custom message if any, then user message) + messages = []; + + // Add user message + const userContent: (TextContent | ImageContent)[] = [{ type: "text", text: expandedText }]; + if (currentImages) { + userContent.push(...currentImages); + } + messages.push({ + role: "user", + content: userContent, + timestamp: Date.now(), + }); + + // Inject any pending "nextTurn" messages as context alongside the user message + for (const msg of this.pendingNextTurnMessages) { + messages.push(msg); + } + this.pendingNextTurnMessages = []; + + // Emit before_agent_start extension event + const result = await this.currentExtensionRunner.emitBeforeAgentStart( + expandedText, + currentImages, + this.baseSystemPrompt, + this.baseSystemPromptOptions, + ); + // Add all custom messages from extensions + if (result?.messages) { + for (const msg of result.messages) { + messages.push({ + role: "custom", + customType: msg.customType, + content: msg.content, + display: msg.display, + details: msg.details, + timestamp: Date.now(), + }); + } + } + // Apply extension-modified system prompt, or reset to base + if (result?.systemPrompt) { + this.agent.state.systemPrompt = result.systemPrompt; + } else { + // Ensure we're using the base prompt (in case previous turn had modifications) + this.agent.state.systemPrompt = this.baseSystemPrompt; + } + } catch (error) { + preflightResult?.(false); + throw error; + } + + if (!messages) { + return; + } + + preflightResult?.(true); + await this.runAgentPrompt(messages); + } + + /** + * Try to execute an extension command. Returns true if command was found and executed. + */ + private async tryExecuteExtensionCommand(text: string): Promise { + // Parse command name and args + const spaceIndex = text.indexOf(" "); + const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex); + const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1); + + const command = this.currentExtensionRunner.getCommand(commandName); + if (!command) { + return false; + } + + // Get command context from extension runner (includes session control methods) + const ctx = this.currentExtensionRunner.createCommandContext(); + + try { + await command.handler(args, ctx); + return true; + } catch (err) { + // Emit error via extension runner + this.currentExtensionRunner.emitError({ + extensionPath: `command:${commandName}`, + event: "command", + error: err instanceof Error ? err.message : String(err), + }); + return true; + } + } + + /** + * Expand skill commands (/skill:name args) to their full content. + * Returns the expanded text, or the original text if not a skill command or skill not found. + * Emits errors via extension runner if file read fails. + */ + private expandSkillCommand(text: string): string { + if (!text.startsWith("/skill:")) { + return text; + } + + const spaceIndex = text.indexOf(" "); + const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex); + const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim(); + + const skill = this.sessionResourceLoader.getSkills().skills.find((s) => s.name === skillName); + if (!skill) { + return text; + } // Unknown skill, pass through + + try { + const content = readFileSync(skill.filePath, "utf-8"); + const body = stripFrontmatter(content).trim(); + const skillBlock = `\nReferences are relative to ${skill.baseDir}.\n\n${body}\n`; + return args ? `${skillBlock}\n\n${args}` : skillBlock; + } catch (err) { + // Emit error like extension commands do + this.currentExtensionRunner.emitError({ + extensionPath: skill.filePath, + event: "skill_expansion", + error: err instanceof Error ? err.message : String(err), + }); + return text; // Return original on error + } + } + + /** + * Queue a steering message while the agent is running. + * Delivered after the current assistant turn finishes executing its tool calls, + * before the next LLM call. + * Expands skill commands and prompt templates. Errors on extension commands. + * @param images Optional image attachments to include with the message + * @throws Error if text is an extension command + */ + async steer(text: string, images?: ImageContent[]): Promise { + // Check for extension commands (cannot be queued) + if (text.startsWith("/")) { + this.throwIfExtensionCommand(text); + } + + // Expand skill commands and prompt templates + let expandedText = this.expandSkillCommand(text); + expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]); + + await this.queueSteer(expandedText, images); + } + + /** + * Queue a follow-up message to be processed after the agent finishes. + * Delivered only when agent has no more tool calls or steering messages. + * Expands skill commands and prompt templates. Errors on extension commands. + * @param images Optional image attachments to include with the message + * @throws Error if text is an extension command + */ + async followUp(text: string, images?: ImageContent[]): Promise { + // Check for extension commands (cannot be queued) + if (text.startsWith("/")) { + this.throwIfExtensionCommand(text); + } + + // Expand skill commands and prompt templates + let expandedText = this.expandSkillCommand(text); + expandedText = expandPromptTemplate(expandedText, [...this.promptTemplates]); + + await this.queueFollowUp(expandedText, images); + } + + /** + * Internal: Queue a steering message (already expanded, no extension command check). + */ + private async queueSteer(text: string, images?: ImageContent[]): Promise { + this.steeringMessages.push(text); + this.emitQueueUpdate(); + const content: (TextContent | ImageContent)[] = [{ type: "text", text }]; + if (images) { + content.push(...images); + } + this.agent.steer({ + role: "user", + content, + timestamp: Date.now(), + }); + } + + /** + * Internal: Queue a follow-up message (already expanded, no extension command check). + */ + private async queueFollowUp(text: string, images?: ImageContent[]): Promise { + this.followUpMessages.push(text); + this.emitQueueUpdate(); + const content: (TextContent | ImageContent)[] = [{ type: "text", text }]; + if (images) { + content.push(...images); + } + this.agent.followUp({ + role: "user", + content, + timestamp: Date.now(), + }); + } + + /** + * Throw an error if the text is an extension command. + */ + private throwIfExtensionCommand(text: string): void { + const spaceIndex = text.indexOf(" "); + const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex); + const command = this.currentExtensionRunner.getCommand(commandName); + + if (command) { + throw new Error( + `Extension command "/${commandName}" cannot be queued. Use prompt() or execute the command when not streaming.`, + ); + } + } + + /** + * Send a custom message to the session. Creates a CustomMessageEntry. + * + * Handles three cases: + * - Streaming: queues message, processed when loop pulls from queue + * - Not streaming + triggerTurn: appends to state/session, starts new turn + * - Not streaming + no trigger: appends to state/session, no turn + * + * @param message Custom message with customType, content, display, details + * @param options.triggerTurn If true and not streaming, triggers a new LLM turn + * @param options.deliverAs Delivery mode: "steer", "followUp", or "nextTurn" + */ + async sendCustomMessage( + message: Pick, "customType" | "content" | "display" | "details">, + options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }, + ): Promise { + const appMessage = { + role: "custom" as const, + customType: message.customType, + content: message.content, + display: message.display, + details: message.details, + timestamp: Date.now(), + } satisfies CustomMessage; + if (options?.deliverAs === "nextTurn") { + this.pendingNextTurnMessages.push(appMessage); + } else if (this.isStreaming) { + if (options?.deliverAs === "followUp") { + this.agent.followUp(appMessage); + } else { + this.agent.steer(appMessage); + } + } else if (options?.triggerTurn) { + await this.runAgentPrompt(appMessage); + } else { + this.agent.state.messages.push(appMessage); + this.sessionManager.appendCustomMessageEntry( + message.customType, + message.content, + message.display, + message.details, + ); + this.emit({ type: "message_start", message: appMessage }); + this.emit({ type: "message_end", message: appMessage }); + } + } + + /** + * Send a user message to the agent. Always triggers a turn. + * When the agent is streaming, use deliverAs to specify how to queue the message. + * + * @param content User message content (string or content array) + * @param options.deliverAs Delivery mode when streaming: "steer" or "followUp" + */ + async sendUserMessage( + content: string | (TextContent | ImageContent)[], + options?: { deliverAs?: "steer" | "followUp" }, + ): Promise { + // Normalize content to text string + optional images + let text: string; + let images: ImageContent[] | undefined; + + if (typeof content === "string") { + text = content; + } else { + const textParts: string[] = []; + images = []; + for (const part of content) { + if (part.type === "text") { + textParts.push(part.text); + } else { + images.push(part); + } + } + text = textParts.join("\n"); + if (images.length === 0) { + images = undefined; + } + } + + // Use prompt() with expandPromptTemplates: false to skip command handling and template expansion + await this.prompt(text, { + expandPromptTemplates: false, + streamingBehavior: options?.deliverAs, + images, + source: "extension", + }); + } + + /** + * Clear all queued messages and return them. + * Useful for restoring to editor when user aborts. + * @returns Object with steering and followUp arrays + */ + clearQueue(): { steering: string[]; followUp: string[] } { + const steering = [...this.steeringMessages]; + const followUp = [...this.followUpMessages]; + this.steeringMessages = []; + this.followUpMessages = []; + this.agent.clearAllQueues(); + this.emitQueueUpdate(); + return { steering, followUp }; + } + + /** Number of pending messages (includes both steering and follow-up) */ + get pendingMessageCount(): number { + return this.steeringMessages.length + this.followUpMessages.length; + } + + /** Get pending steering messages (read-only) */ + getSteeringMessages(): readonly string[] { + return this.steeringMessages; + } + + /** Get pending follow-up messages (read-only) */ + getFollowUpMessages(): readonly string[] { + return this.followUpMessages; + } + + get resourceLoader(): ResourceLoader { + return this.sessionResourceLoader; + } + + /** + * Abort current operation and wait for agent to become idle. + */ + async abort(): Promise { + this.abortRetry(); + this.agent.abort(); + await this.agent.waitForIdle(); + } + + // ========================================================================= + // Model Management + // ========================================================================= + + private async emitModelSelect( + nextModel: Model, + previousModel: Model | undefined, + source: "set" | "cycle" | "restore", + ): Promise { + if (modelsAreEqual(previousModel, nextModel)) { + return; + } + await this.currentExtensionRunner.emit({ + type: "model_select", + model: nextModel, + previousModel, + source, + }); + } + + /** + * Set model directly. + * Validates that auth is configured, saves to session and settings. + * @throws Error if no auth is configured for the model + */ + async setModel(model: Model): Promise { + if (!this.sessionModelRegistry.hasConfiguredAuth(model)) { + throw new Error(`No API key for ${model.provider}/${model.id}`); + } + + const previousModel = this.model; + const thinkingLevel = this.getThinkingLevelForModelSwitch(); + this.agent.state.model = model; + this.sessionManager.appendModelChange(model.provider, model.id); + this.settingsManager.setDefaultModelAndProvider(model.provider, model.id); + + // Re-clamp thinking level for new model's capabilities + this.setThinkingLevel(thinkingLevel); + + await this.emitModelSelect(model, previousModel, "set"); + } + + /** + * Cycle to next/previous model. + * Uses scoped models (from --models flag) if available, otherwise all available models. + * @param direction - "forward" (default) or "backward" + * @returns The new model info, or undefined if only one model available + */ + async cycleModel( + direction: "forward" | "backward" = "forward", + ): Promise { + if (this.scopedModelEntries.length > 0) { + return this.cycleScopedModel(direction); + } + return this.cycleAvailableModel(direction); + } + + private async cycleScopedModel( + direction: "forward" | "backward", + ): Promise { + const scopedModels = this.scopedModelEntries.filter((scoped) => + this.sessionModelRegistry.hasConfiguredAuth(scoped.model), + ); + if (scopedModels.length <= 1) { + return undefined; + } + + const currentModel = this.model; + let currentIndex = scopedModels.findIndex((sm) => modelsAreEqual(sm.model, currentModel)); + + if (currentIndex === -1) { + currentIndex = 0; + } + const len = scopedModels.length; + const nextIndex = + direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len; + const next = scopedModels[nextIndex]; + const thinkingLevel = this.getThinkingLevelForModelSwitch(next.thinkingLevel); + + // Apply model + this.agent.state.model = next.model; + this.sessionManager.appendModelChange(next.model.provider, next.model.id); + this.settingsManager.setDefaultModelAndProvider(next.model.provider, next.model.id); + + // Apply thinking level. + // - Explicit scoped model thinking level overrides current session level + // - Undefined scoped model thinking level inherits the current session preference + // setThinkingLevel clamps to model capabilities. + this.setThinkingLevel(thinkingLevel); + + await this.emitModelSelect(next.model, currentModel, "cycle"); + + return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true }; + } + + private async cycleAvailableModel( + direction: "forward" | "backward", + ): Promise { + const availableModels = this.sessionModelRegistry.getAvailable(); + if (availableModels.length <= 1) { + return undefined; + } + + const currentModel = this.model; + let currentIndex = availableModels.findIndex((m) => modelsAreEqual(m, currentModel)); + + if (currentIndex === -1) { + currentIndex = 0; + } + const len = availableModels.length; + const nextIndex = + direction === "forward" ? (currentIndex + 1) % len : (currentIndex - 1 + len) % len; + const nextModel = availableModels[nextIndex]; + + const thinkingLevel = this.getThinkingLevelForModelSwitch(); + this.agent.state.model = nextModel; + this.sessionManager.appendModelChange(nextModel.provider, nextModel.id); + this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id); + + // Re-clamp thinking level for new model's capabilities + this.setThinkingLevel(thinkingLevel); + + await this.emitModelSelect(nextModel, currentModel, "cycle"); + + return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false }; + } + + // ========================================================================= + // Thinking Level Management + // ========================================================================= + + /** + * Set thinking level. + * Clamps to model capabilities based on available thinking levels. + * Saves to session and settings only if the level actually changes. + */ + setThinkingLevel(level: ThinkingLevel): void { + const availableLevels = this.getAvailableThinkingLevels(); + const effectiveLevel = availableLevels.includes(level) ? level : this.clampThinkingLevel(level); + + // Only persist if actually changing + const previousLevel = this.agent.state.thinkingLevel; + const isChanging = effectiveLevel !== previousLevel; + + this.agent.state.thinkingLevel = effectiveLevel; + + if (isChanging) { + this.sessionManager.appendThinkingLevelChange(effectiveLevel); + if (this.supportsThinking() || effectiveLevel !== "off") { + this.settingsManager.setDefaultThinkingLevel(effectiveLevel); + } + this.emit({ type: "thinking_level_changed", level: effectiveLevel }); + void this.currentExtensionRunner.emit({ + type: "thinking_level_select", + level: effectiveLevel, + previousLevel, + }); + } + } + + /** + * Cycle to next thinking level. + * @returns New level, or undefined if model doesn't support thinking + */ + cycleThinkingLevel(): ThinkingLevel | undefined { + if (!this.supportsThinking()) { + return undefined; + } + + const levels = this.getAvailableThinkingLevels(); + const currentIndex = levels.indexOf(this.thinkingLevel); + const nextIndex = (currentIndex + 1) % levels.length; + const nextLevel = levels[nextIndex]; + + this.setThinkingLevel(nextLevel); + return nextLevel; + } + + /** + * Get available thinking levels for current model. + * The provider will clamp to what the specific model supports internally. + */ + getAvailableThinkingLevels(): ThinkingLevel[] { + if (!this.model) { + return THINKING_LEVELS; + } + return getSupportedThinkingLevels(this.model) as ThinkingLevel[]; + } + + /** + * Check if current model supports thinking/reasoning. + */ + supportsThinking(): boolean { + return !!this.model?.reasoning; + } + + private getThinkingLevelForModelSwitch(explicitLevel?: ThinkingLevel): ThinkingLevel { + if (explicitLevel !== undefined) { + return explicitLevel; + } + if (!this.supportsThinking()) { + return this.settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL; + } + return this.thinkingLevel; + } + + private clampThinkingLevel(level: ThinkingLevel): ThinkingLevel { + return this.model ? (clampThinkingLevel(this.model, level) as ThinkingLevel) : "off"; + } + + // ========================================================================= + // Queue Mode Management + // ========================================================================= + + /** + * Set steering message mode. + * Saves to settings. + */ + setSteeringMode(mode: "all" | "one-at-a-time"): void { + this.agent.steeringMode = mode; + this.settingsManager.setSteeringMode(mode); + } + + /** + * Set follow-up message mode. + * Saves to settings. + */ + setFollowUpMode(mode: "all" | "one-at-a-time"): void { + this.agent.followUpMode = mode; + this.settingsManager.setFollowUpMode(mode); + } + + // ========================================================================= + // Compaction + // ========================================================================= + + /** + * Manually compact the session context. + * Aborts current agent operation first. + * @param customInstructions Optional instructions for the compaction summary + */ + async compact(customInstructions?: string): Promise { + this.disconnectFromAgent(); + await this.abort(); + this.compactionAbortController = new AbortController(); + this.emit({ type: "compaction_start", reason: "manual" }); + + try { + if (!this.model) { + throw new Error(formatNoModelSelectedMessage()); + } + + const { apiKey, headers } = await this.getCompactionRequestAuth(this.model); + + const pathEntries = this.sessionManager.getBranch(); + const settings = this.settingsManager.getCompactionSettings(); + + const preparation = prepareCompaction(pathEntries, settings); + if (!preparation) { + // Check why we can't compact + const lastEntry = pathEntries[pathEntries.length - 1]; + if (lastEntry?.type === "compaction") { + throw new Error("Already compacted"); + } + throw new Error("Nothing to compact (session too small)"); + } + + let extensionCompaction: CompactionResult | undefined; + let fromExtension = false; + + if (this.currentExtensionRunner.hasHandlers("session_before_compact")) { + const result = await this.currentExtensionRunner.emit({ + type: "session_before_compact", + preparation, + branchEntries: pathEntries, + customInstructions, + signal: this.compactionAbortController.signal, + }); + + if (result?.cancel) { + throw new Error("Compaction cancelled"); + } + + if (result?.compaction) { + extensionCompaction = result.compaction; + fromExtension = true; + } + } + + let summary: string; + let firstKeptEntryId: string; + let tokensBefore: number; + let details: unknown; + + if (extensionCompaction) { + // Extension provided compaction content + summary = extensionCompaction.summary; + firstKeptEntryId = extensionCompaction.firstKeptEntryId; + tokensBefore = extensionCompaction.tokensBefore; + details = extensionCompaction.details; + } else { + // Generate compaction result + const result = await compact( + preparation, + this.model, + apiKey, + headers, + customInstructions, + this.compactionAbortController.signal, + this.thinkingLevel, + this.agent.streamFn, + ); + summary = result.summary; + firstKeptEntryId = result.firstKeptEntryId; + tokensBefore = result.tokensBefore; + details = result.details; + } + + if (this.compactionAbortController.signal.aborted) { + throw new Error("Compaction cancelled"); + } + + this.sessionManager.appendCompaction( + summary, + firstKeptEntryId, + tokensBefore, + details, + fromExtension, + ); + const newEntries = this.sessionManager.getEntries(); + const sessionContext = this.sessionManager.buildSessionContext(); + this.agent.state.messages = sessionContext.messages; + + // Get the saved compaction entry for the extension event + const savedCompactionEntry = newEntries.find( + (e) => e.type === "compaction" && e.summary === summary, + ) as CompactionEntry | undefined; + + if (this.currentExtensionRunner && savedCompactionEntry) { + await this.currentExtensionRunner.emit({ + type: "session_compact", + compactionEntry: savedCompactionEntry, + fromExtension, + }); + } + + const compactionResult = { + summary, + firstKeptEntryId, + tokensBefore, + details, + }; + this.emit({ + type: "compaction_end", + reason: "manual", + result: compactionResult, + aborted: false, + willRetry: false, + }); + return compactionResult; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const aborted = + message === "Compaction cancelled" || + (error instanceof Error && error.name === "AbortError"); + this.emit({ + type: "compaction_end", + reason: "manual", + result: undefined, + aborted, + willRetry: false, + errorMessage: aborted ? undefined : `Compaction failed: ${message}`, + }); + throw error; + } finally { + this.compactionAbortController = undefined; + this.reconnectToAgent(); + } + } + + /** + * Cancel in-progress compaction (manual or auto). + */ + abortCompaction(): void { + this.compactionAbortController?.abort(); + this.autoCompactionAbortController?.abort(); + } + + /** + * Cancel in-progress branch summarization. + */ + abortBranchSummary(): void { + this.branchSummaryAbortController?.abort(); + } + + /** + * Check if compaction is needed and run it. + * Called after agent_end and before prompt submission. + * + * Two cases: + * 1. Overflow: LLM returned context overflow error, remove error message from agent state, compact, auto-retry + * 2. Threshold: Context over threshold, compact, NO auto-retry (user continues manually) + * + * @param assistantMessage The assistant message to check + * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true + */ + private async checkCompaction( + assistantMessage: AssistantMessage, + skipAbortedCheck = true, + ): Promise { + const settings = this.settingsManager.getCompactionSettings(); + if (!settings.enabled) { + return false; + } + + // Skip if message was aborted (user cancelled) - unless skipAbortedCheck is false + if (skipAbortedCheck && assistantMessage.stopReason === "aborted") { + return false; + } + + const contextWindow = this.model?.contextWindow ?? 0; + + // Skip overflow check if the message came from a different model. + // This handles the case where user switched from a smaller-context model (e.g. opus) + // to a larger-context model (e.g. codex) - the overflow error from the old model + // shouldn't trigger compaction for the new model. + const sameModel = + this.model && + assistantMessage.provider === this.model.provider && + assistantMessage.model === this.model.id; + + // Skip compaction checks if this assistant message is older than the latest + // compaction boundary. This prevents a stale pre-compaction usage/error + // from retriggering compaction on the first prompt after compaction. + const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch()); + const assistantIsFromBeforeCompaction = + compactionEntry !== null && + assistantMessage.timestamp <= new Date(compactionEntry.timestamp).getTime(); + if (assistantIsFromBeforeCompaction) { + return false; + } + + // Case 1: Overflow - LLM returned context overflow error + if (sameModel && isContextOverflow(assistantMessage, contextWindow)) { + if (this.overflowRecoveryAttempted) { + this.emit({ + type: "compaction_end", + reason: "overflow", + result: undefined, + aborted: false, + willRetry: false, + errorMessage: + "Context overflow recovery failed after one compact-and-retry attempt. Try reducing context or switching to a larger-context model.", + }); + return false; + } + + this.overflowRecoveryAttempted = true; + // Remove the error message from agent state (it IS saved to session for history, + // but we don't want it in context for the retry) + const messages = this.agent.state.messages; + if (messages.length > 0 && messages[messages.length - 1].role === "assistant") { + this.agent.state.messages = messages.slice(0, -1); + } + return await this.runAutoCompaction("overflow", true); + } + + // Case 2: Threshold - context is getting large + // For error messages (no usage data), estimate from last successful response. + // This ensures sessions that hit persistent API errors (e.g. 529) can still compact. + let contextTokens: number; + if (assistantMessage.stopReason === "error") { + const messages = this.agent.state.messages; + const estimate = estimateContextTokens(messages); + if (estimate.lastUsageIndex === null) { + return false; + } // No usage data at all + // Verify the usage source is post-compaction. Kept pre-compaction messages + // have stale usage reflecting the old (larger) context and would falsely + // trigger compaction right after one just finished. + const usageMsg = messages[estimate.lastUsageIndex]; + if ( + compactionEntry && + usageMsg.role === "assistant" && + usageMsg.timestamp <= new Date(compactionEntry.timestamp).getTime() + ) { + return false; + } + contextTokens = estimate.tokens; + } else { + contextTokens = calculateContextTokens(assistantMessage.usage); + } + if (shouldCompact(contextTokens, contextWindow, settings)) { + return await this.runAutoCompaction("threshold", false); + } + return false; + } + + /** + * Internal: Run auto-compaction with events. + */ + private async runAutoCompaction( + reason: "overflow" | "threshold", + willRetry: boolean, + ): Promise { + const settings = this.settingsManager.getCompactionSettings(); + + this.emit({ type: "compaction_start", reason }); + this.autoCompactionAbortController = new AbortController(); + + try { + if (!this.model) { + this.emit({ + type: "compaction_end", + reason, + result: undefined, + aborted: false, + willRetry: false, + }); + return false; + } + + let apiKey: string | undefined; + let headers: Record | undefined; + if (this.agent.streamFn === streamSimple) { + const authResult = await this.sessionModelRegistry.getApiKeyAndHeaders(this.model); + if (!authResult.ok || !authResult.apiKey) { + this.emit({ + type: "compaction_end", + reason, + result: undefined, + aborted: false, + willRetry: false, + }); + return false; + } + apiKey = authResult.apiKey; + headers = authResult.headers; + } else { + ({ apiKey, headers } = await this.getCompactionRequestAuth(this.model)); + } + + const pathEntries = this.sessionManager.getBranch(); + + const preparation = prepareCompaction(pathEntries, settings); + if (!preparation) { + this.emit({ + type: "compaction_end", + reason, + result: undefined, + aborted: false, + willRetry: false, + }); + return false; + } + + let extensionCompaction: CompactionResult | undefined; + let fromExtension = false; + + if (this.currentExtensionRunner.hasHandlers("session_before_compact")) { + const extensionResult = await this.currentExtensionRunner.emit({ + type: "session_before_compact", + preparation, + branchEntries: pathEntries, + customInstructions: undefined, + signal: this.autoCompactionAbortController.signal, + }); + + if (extensionResult?.cancel) { + this.emit({ + type: "compaction_end", + reason, + result: undefined, + aborted: true, + willRetry: false, + }); + return false; + } + + if (extensionResult?.compaction) { + extensionCompaction = extensionResult.compaction; + fromExtension = true; + } + } + + let summary: string; + let firstKeptEntryId: string; + let tokensBefore: number; + let details: unknown; + + if (extensionCompaction) { + // Extension provided compaction content + summary = extensionCompaction.summary; + firstKeptEntryId = extensionCompaction.firstKeptEntryId; + tokensBefore = extensionCompaction.tokensBefore; + details = extensionCompaction.details; + } else { + // Generate compaction result + const compactResult = await compact( + preparation, + this.model, + apiKey, + headers, + undefined, + this.autoCompactionAbortController.signal, + this.thinkingLevel, + this.agent.streamFn, + ); + summary = compactResult.summary; + firstKeptEntryId = compactResult.firstKeptEntryId; + tokensBefore = compactResult.tokensBefore; + details = compactResult.details; + } + + if (this.autoCompactionAbortController.signal.aborted) { + this.emit({ + type: "compaction_end", + reason, + result: undefined, + aborted: true, + willRetry: false, + }); + return false; + } + + this.sessionManager.appendCompaction( + summary, + firstKeptEntryId, + tokensBefore, + details, + fromExtension, + ); + const newEntries = this.sessionManager.getEntries(); + const sessionContext = this.sessionManager.buildSessionContext(); + this.agent.state.messages = sessionContext.messages; + + // Get the saved compaction entry for the extension event + const savedCompactionEntry = newEntries.find( + (e) => e.type === "compaction" && e.summary === summary, + ) as CompactionEntry | undefined; + + if (this.currentExtensionRunner && savedCompactionEntry) { + await this.currentExtensionRunner.emit({ + type: "session_compact", + compactionEntry: savedCompactionEntry, + fromExtension, + }); + } + + const result: CompactionResult = { + summary, + firstKeptEntryId, + tokensBefore, + details, + }; + this.emit({ type: "compaction_end", reason, result, aborted: false, willRetry }); + + if (willRetry) { + const messages = this.agent.state.messages; + const lastMsg = messages[messages.length - 1]; + if (lastMsg?.role === "assistant" && lastMsg.stopReason === "error") { + this.agent.state.messages = messages.slice(0, -1); + } + return true; + } + + // Auto-compaction can complete while follow-up/steering/custom messages are waiting. + // Continue once so queued messages are delivered. + return this.agent.hasQueuedMessages(); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "compaction failed"; + this.emit({ + type: "compaction_end", + reason, + result: undefined, + aborted: false, + willRetry: false, + errorMessage: + reason === "overflow" + ? `Context overflow recovery failed: ${errorMessage}` + : `Auto-compaction failed: ${errorMessage}`, + }); + return false; + } finally { + this.autoCompactionAbortController = undefined; + } + } + + /** + * Toggle auto-compaction setting. + */ + setAutoCompactionEnabled(enabled: boolean): void { + this.settingsManager.setCompactionEnabled(enabled); + } + + /** Whether auto-compaction is enabled */ + get autoCompactionEnabled(): boolean { + return this.settingsManager.getCompactionEnabled(); + } + + async bindExtensions(bindings: ExtensionBindings): Promise { + if (bindings.uiContext !== undefined) { + this.extensionUIContext = bindings.uiContext; + } + if (bindings.commandContextActions !== undefined) { + this.extensionCommandContextActions = bindings.commandContextActions; + } + if (bindings.abortHandler !== undefined) { + this.extensionAbortHandler = bindings.abortHandler; + } + if (bindings.shutdownHandler !== undefined) { + this.extensionShutdownHandler = bindings.shutdownHandler; + } + if (bindings.onError !== undefined) { + this.extensionErrorListener = bindings.onError; + } + + this.applyExtensionBindings(this.currentExtensionRunner); + await this.currentExtensionRunner.emit(this.sessionStartEvent); + await this.extendResourcesFromExtensions( + this.sessionStartEvent.reason === "reload" ? "reload" : "startup", + ); + } + + private async extendResourcesFromExtensions(reason: "startup" | "reload"): Promise { + if (!this.currentExtensionRunner.hasHandlers("resources_discover")) { + return; + } + + const { skillPaths, promptPaths, themePaths } = + await this.currentExtensionRunner.emitResourcesDiscover(this.cwd, reason); + + if (skillPaths.length === 0 && promptPaths.length === 0 && themePaths.length === 0) { + return; + } + + const extensionPaths: ResourceExtensionPaths = { + skillPaths: this.buildExtensionResourcePaths(skillPaths), + promptPaths: this.buildExtensionResourcePaths(promptPaths), + themePaths: this.buildExtensionResourcePaths(themePaths), + }; + + this.sessionResourceLoader.extendResources(extensionPaths); + this.baseSystemPrompt = this.rebuildSystemPrompt(this.getActiveToolNames()); + this.agent.state.systemPrompt = this.baseSystemPrompt; + } + + private buildExtensionResourcePaths( + entries: Array<{ path: string; extensionPath: string }>, + ): Array<{ + path: string; + metadata: { source: string; scope: "temporary"; origin: "top-level"; baseDir?: string }; + }> { + return entries.map((entry) => { + const source = this.getExtensionSourceLabel(entry.extensionPath); + const baseDir = entry.extensionPath.startsWith("<") + ? undefined + : dirname(entry.extensionPath); + return { + path: entry.path, + metadata: { + source, + scope: "temporary", + origin: "top-level", + baseDir, + }, + }; + }); + } + + private getExtensionSourceLabel(extensionPath: string): string { + if (extensionPath.startsWith("<")) { + return `extension:${extensionPath.replace(/[<>]/g, "")}`; + } + const base = basename(extensionPath); + const name = base.replace(/\.(ts|js)$/, ""); + return `extension:${name}`; + } + + private applyExtensionBindings(runner: ExtensionRunner): void { + runner.setUIContext(this.extensionUIContext); + runner.bindCommandContext(this.extensionCommandContextActions); + + this.extensionErrorUnsubscriber?.(); + this.extensionErrorUnsubscriber = this.extensionErrorListener + ? runner.onError(this.extensionErrorListener) + : undefined; + } + + private refreshCurrentModelFromRegistry(): void { + const currentModel = this.model; + if (!currentModel) { + return; + } + + const refreshedModel = this.sessionModelRegistry.find(currentModel.provider, currentModel.id); + if (!refreshedModel || refreshedModel === currentModel) { + return; + } + + this.agent.state.model = refreshedModel; + } + + private bindExtensionCore(runner: ExtensionRunner): void { + const getCommands = (): SlashCommandInfo[] => { + const extensionCommands: SlashCommandInfo[] = runner + .getRegisteredCommands() + .map((command) => ({ + name: command.invocationName, + description: command.description, + source: "extension", + sourceInfo: command.sourceInfo, + })); + + const templates: SlashCommandInfo[] = this.promptTemplates.map((template) => ({ + name: template.name, + description: template.description, + source: "prompt", + sourceInfo: template.sourceInfo, + })); + + const skills: SlashCommandInfo[] = this.sessionResourceLoader + .getSkills() + .skills.map((skill) => ({ + name: `skill:${skill.name}`, + description: skill.description, + source: "skill", + sourceInfo: skill.sourceInfo, + })); + + return [...extensionCommands, ...templates, ...skills]; + }; + + runner.bindCore( + { + sendMessage: (message, options) => { + this.sendCustomMessage(message, options).catch((err) => { + runner.emitError({ + extensionPath: "", + event: "send_message", + error: err instanceof Error ? err.message : String(err), + }); + }); + }, + sendUserMessage: (content, options) => { + this.sendUserMessage(content, options).catch((err) => { + runner.emitError({ + extensionPath: "", + event: "send_user_message", + error: err instanceof Error ? err.message : String(err), + }); + }); + }, + appendEntry: (customType, data) => { + this.sessionManager.appendCustomEntry(customType, data); + }, + setSessionName: (name) => { + this.setSessionName(name); + }, + getSessionName: () => { + return this.sessionManager.getSessionName(); + }, + setLabel: (entryId, label) => { + this.sessionManager.appendLabelChange(entryId, label); + }, + getActiveTools: () => this.getActiveToolNames(), + getAllTools: () => this.getAllTools(), + setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames), + refreshTools: () => this.refreshToolRegistry(), + getCommands, + setModel: async (model) => { + if (!this.sessionModelRegistry.hasConfiguredAuth(model)) { + return false; + } + await this.setModel(model); + return true; + }, + getThinkingLevel: () => this.thinkingLevel, + setThinkingLevel: (level) => this.setThinkingLevel(level), + }, + { + getModel: () => this.model, + isIdle: () => !this.isStreaming, + getSignal: () => this.agent.signal, + abort: () => { + if (this.extensionAbortHandler) { + this.extensionAbortHandler(); + return; + } + void this.abort(); + }, + hasPendingMessages: () => this.pendingMessageCount > 0, + shutdown: () => { + this.extensionShutdownHandler?.(); + }, + getContextUsage: () => this.getContextUsage(), + compact: (options) => { + void (async () => { + try { + const result = await this.compact(options?.customInstructions); + options?.onComplete?.(result); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + options?.onError?.(err); + } + })(); + }, + getSystemPrompt: () => this.systemPrompt, + }, + { + registerProvider: (name, config) => { + this.sessionModelRegistry.registerProvider(name, config); + this.refreshCurrentModelFromRegistry(); + }, + unregisterProvider: (name) => { + this.sessionModelRegistry.unregisterProvider(name); + this.refreshCurrentModelFromRegistry(); + }, + }, + ); + } + + private refreshToolRegistry(options?: { + activeToolNames?: string[]; + includeAllExtensionTools?: boolean; + }): void { + const previousRegistryNames = new Set(this.toolRegistry.keys()); + const previousActiveToolNames = this.getActiveToolNames(); + const allowedToolNames = this.allowedToolNames; + const isAllowedTool = (name: string): boolean => + !allowedToolNames || allowedToolNames.has(name); + + const registeredTools = this.currentExtensionRunner.getAllRegisteredTools(); + const allCustomTools = [ + ...registeredTools, + ...this.customTools.map((definition) => ({ + definition, + sourceInfo: createSyntheticSourceInfo(``, { source: "sdk" }), + })), + ].filter((tool) => isAllowedTool(tool.definition.name)); + const definitionRegistry = new Map( + Array.from(this.baseToolDefinitions.entries()) + .filter(([name]) => isAllowedTool(name)) + .map(([name, definition]) => [ + name, + { + definition, + sourceInfo: createSyntheticSourceInfo(``, { source: "builtin" }), + }, + ]), + ); + for (const tool of allCustomTools) { + definitionRegistry.set(tool.definition.name, { + definition: tool.definition, + sourceInfo: tool.sourceInfo, + }); + } + this.toolDefinitions = definitionRegistry; + this.toolPromptSnippets = new Map( + Array.from(definitionRegistry.values()) + .map(({ definition }) => { + const snippet = this.normalizePromptSnippet(definition.promptSnippet); + return snippet ? ([definition.name, snippet] as const) : undefined; + }) + .filter((entry): entry is readonly [string, string] => entry !== undefined), + ); + this.toolPromptGuidelines = new Map( + Array.from(definitionRegistry.values()) + .map(({ definition }) => { + const guidelines = this.normalizePromptGuidelines(definition.promptGuidelines); + return guidelines.length > 0 ? ([definition.name, guidelines] as const) : undefined; + }) + .filter((entry): entry is readonly [string, string[]] => entry !== undefined), + ); + const runner = this.currentExtensionRunner; + const wrappedExtensionTools = wrapRegisteredTools(allCustomTools, runner); + const wrappedBuiltInTools = wrapRegisteredTools( + Array.from(this.baseToolDefinitions.values()) + .filter((definition) => isAllowedTool(definition.name)) + .map((definition) => ({ + definition, + sourceInfo: createSyntheticSourceInfo(``, { + source: "builtin", + }), + })), + runner, + ); + + const toolRegistry = new Map(wrappedBuiltInTools.map((tool) => [tool.name, tool])); + for (const tool of wrappedExtensionTools) { + toolRegistry.set(tool.name, tool); + } + this.toolRegistry = toolRegistry; + + const nextActiveToolNames = ( + options?.activeToolNames ? [...options.activeToolNames] : [...previousActiveToolNames] + ).filter((name) => isAllowedTool(name)); + + if (allowedToolNames) { + for (const toolName of this.toolRegistry.keys()) { + if (allowedToolNames.has(toolName)) { + nextActiveToolNames.push(toolName); + } + } + } else if (options?.includeAllExtensionTools) { + for (const tool of wrappedExtensionTools) { + nextActiveToolNames.push(tool.name); + } + } else if (!options?.activeToolNames) { + for (const toolName of this.toolRegistry.keys()) { + if (!previousRegistryNames.has(toolName)) { + nextActiveToolNames.push(toolName); + } + } + } + + this.setActiveToolsByName([...new Set(nextActiveToolNames)]); + } + + private buildRuntime(options: { + activeToolNames?: string[]; + flagValues?: Map; + includeAllExtensionTools?: boolean; + }): void { + const autoResizeImages = this.settingsManager.getImageAutoResize(); + const shellCommandPrefix = this.settingsManager.getShellCommandPrefix(); + const shellPath = this.settingsManager.getShellPath(); + const baseToolDefinitions = this.baseToolsOverride + ? Object.fromEntries( + Object.entries(this.baseToolsOverride).map(([name, tool]) => [ + name, + createToolDefinitionFromAgentTool(tool), + ]), + ) + : createAllToolDefinitions(this.cwd, { + read: { autoResizeImages }, + bash: { commandPrefix: shellCommandPrefix, shellPath }, + }); + + this.baseToolDefinitions = new Map( + Object.entries(baseToolDefinitions).map(([name, tool]) => [name, tool as ToolDefinition]), + ); + + const extensionsResult = this.sessionResourceLoader.getExtensions(); + if (options.flagValues) { + for (const [name, value] of options.flagValues) { + extensionsResult.runtime.flagValues.set(name, value); + } + } + + this.currentExtensionRunner = new ExtensionRunner( + extensionsResult.extensions, + extensionsResult.runtime, + this.cwd, + this.sessionManager, + this.sessionModelRegistry, + ); + if (this.extensionRunnerRef) { + this.extensionRunnerRef.current = this.currentExtensionRunner; + } + this.bindExtensionCore(this.currentExtensionRunner); + this.applyExtensionBindings(this.currentExtensionRunner); + + const defaultActiveToolNames = this.baseToolsOverride + ? Object.keys(this.baseToolsOverride) + : ["read", "bash", "edit", "write"]; + const baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames; + this.refreshToolRegistry({ + activeToolNames: baseActiveToolNames, + includeAllExtensionTools: options.includeAllExtensionTools, + }); + } + + async reload(): Promise { + const previousFlagValues = this.currentExtensionRunner.getFlagValues(); + await emitSessionShutdownEvent(this.currentExtensionRunner, { + type: "session_shutdown", + reason: "reload", + }); + await this.settingsManager.reload(); + resetApiProviders(); + await this.sessionResourceLoader.reload(); + this.buildRuntime({ + activeToolNames: this.getActiveToolNames(), + flagValues: previousFlagValues, + includeAllExtensionTools: true, + }); + + const hasBindings = + this.extensionUIContext || + this.extensionCommandContextActions || + this.extensionShutdownHandler || + this.extensionErrorListener; + if (hasBindings) { + await this.currentExtensionRunner.emit({ type: "session_start", reason: "reload" }); + await this.extendResourcesFromExtensions("reload"); + } + } + + // ========================================================================= + // Auto-Retry + // ========================================================================= + + /** + * Check if an error is retryable (overloaded, rate limit, server errors). + * Context overflow errors are NOT retryable (handled by compaction instead). + */ + private isRetryableError(message: AssistantMessage): boolean { + if (message.stopReason !== "error" || !message.errorMessage) { + return false; + } + + // Context overflow is handled by compaction, not retry + const contextWindow = this.model?.contextWindow ?? 0; + if (isContextOverflow(message, contextWindow)) { + return false; + } + + const err = message.errorMessage; + // Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, premature stream endings, HTTP/2 closed before response, terminated, retry delay exceeded + return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay/i.test( + err, + ); + } + + /** + * Prepare a retryable error for continuation with exponential backoff. + * @returns true if the caller should continue the agent, false otherwise + */ + private async prepareRetry(message: AssistantMessage): Promise { + const settings = this.settingsManager.getRetrySettings(); + if (!settings.enabled) { + return false; + } + + this.retryCount++; + + if (this.retryCount > settings.maxRetries) { + // Preserve the completed attempt count so post-run handling can emit the final failure. + this.retryCount--; + return false; + } + + const delayMs = settings.baseDelayMs * 2 ** (this.retryCount - 1); + + this.emit({ + type: "auto_retry_start", + attempt: this.retryCount, + maxAttempts: settings.maxRetries, + delayMs, + errorMessage: message.errorMessage || "Unknown error", + }); + + // Remove error message from agent state (keep in session for history) + const messages = this.agent.state.messages; + if (messages.length > 0 && messages[messages.length - 1].role === "assistant") { + this.agent.state.messages = messages.slice(0, -1); + } + + // Wait with exponential backoff (abortable) + this.retryAbortController = new AbortController(); + try { + await sleep(delayMs, this.retryAbortController.signal); + } catch { + // Aborted during sleep - emit end event so UI can clean up + const attempt = this.retryCount; + this.retryCount = 0; + this.emit({ + type: "auto_retry_end", + success: false, + attempt, + finalError: "Retry cancelled", + }); + return false; + } finally { + this.retryAbortController = undefined; + } + + return true; + } + + /** + * Cancel in-progress retry. + */ + abortRetry(): void { + this.retryAbortController?.abort(); + } + + /** Whether auto-retry is currently in progress */ + get isRetrying(): boolean { + return this.retryAbortController !== undefined; + } + + /** Whether auto-retry is enabled */ + get autoRetryEnabled(): boolean { + return this.settingsManager.getRetryEnabled(); + } + + /** + * Toggle auto-retry setting. + */ + setAutoRetryEnabled(enabled: boolean): void { + this.settingsManager.setRetryEnabled(enabled); + } + + // ========================================================================= + // Bash Execution + // ========================================================================= + + /** + * Execute a bash command. + * Adds result to agent context and session. + * @param command The bash command to execute + * @param onChunk Optional streaming callback for output + * @param options.excludeFromContext If true, command output won't be sent to LLM (!! prefix) + * @param options.operations Custom BashOperations for remote execution + */ + async executeBash( + command: string, + onChunk?: (chunk: string) => void, + options?: { excludeFromContext?: boolean; operations?: BashOperations }, + ): Promise { + this.bashAbortController = new AbortController(); + + // Apply command prefix if configured (e.g., "shopt -s expand_aliases" for alias support) + const prefix = this.settingsManager.getShellCommandPrefix(); + const shellPath = this.settingsManager.getShellPath(); + const resolvedCommand = prefix ? `${prefix}\n${command}` : command; + + try { + const result = await executeBashWithOperations( + resolvedCommand, + this.sessionManager.getCwd(), + options?.operations ?? createLocalBashOperations({ shellPath }), + { + onChunk, + signal: this.bashAbortController.signal, + }, + ); + + this.recordBashResult(command, result, options); + return result; + } finally { + this.bashAbortController = undefined; + } + } + + /** + * Record a bash execution result in session history. + * Used by executeBash and by extensions that handle bash execution themselves. + */ + recordBashResult( + command: string, + result: BashResult, + options?: { excludeFromContext?: boolean }, + ): void { + const bashMessage: BashExecutionMessage = { + role: "bashExecution", + command, + output: result.output, + exitCode: result.exitCode, + cancelled: result.cancelled, + truncated: result.truncated, + fullOutputPath: result.fullOutputPath, + timestamp: Date.now(), + excludeFromContext: options?.excludeFromContext, + }; + + // If agent is streaming, defer adding to avoid breaking tool_use/tool_result ordering + if (this.isStreaming) { + // Queue for later - will be flushed on agent_end + this.pendingBashMessages.push(bashMessage); + } else { + // Add to agent state immediately + this.agent.state.messages.push(bashMessage); + + // Save to session + this.sessionManager.appendMessage(bashMessage); + } + } + + /** + * Cancel running bash command. + */ + abortBash(): void { + this.bashAbortController?.abort(); + } + + /** Whether a bash command is currently running */ + get isBashRunning(): boolean { + return this.bashAbortController !== undefined; + } + + /** Whether there are pending bash messages waiting to be flushed */ + get hasPendingBashMessages(): boolean { + return this.pendingBashMessages.length > 0; + } + + /** + * Flush pending bash messages to agent state and session. + * Called after agent turn completes to maintain proper message ordering. + */ + private flushPendingBashMessages(): void { + if (this.pendingBashMessages.length === 0) { + return; + } + + for (const bashMessage of this.pendingBashMessages) { + // Add to agent state + this.agent.state.messages.push(bashMessage); + + // Save to session + this.sessionManager.appendMessage(bashMessage); + } + + this.pendingBashMessages = []; + } + + // ========================================================================= + // Session Management + // ========================================================================= + + /** + * Set a display name for the current session. + */ + setSessionName(name: string): void { + this.sessionManager.appendSessionInfo(name); + this.emit({ type: "session_info_changed", name: this.sessionManager.getSessionName() }); + } + + // ========================================================================= + // Tree Navigation + // ========================================================================= + + /** + * Navigate to a different node in the session tree. + * Unlike fork() which creates a new session file, this stays in the same file. + * + * @param targetId The entry ID to navigate to + * @param options.summarize Whether user wants to summarize abandoned branch + * @param options.customInstructions Custom instructions for summarizer + * @param options.replaceInstructions If true, customInstructions replaces the default prompt + * @param options.label Label to attach to the branch summary entry + * @returns Result with editorText (if user message) and cancelled status + */ + async navigateTree( + targetId: string, + options: { + summarize?: boolean; + customInstructions?: string; + replaceInstructions?: boolean; + label?: string; + } = {}, + ): Promise<{ + editorText?: string; + cancelled: boolean; + aborted?: boolean; + summaryEntry?: BranchSummaryEntry; + }> { + const oldLeafId = this.sessionManager.getLeafId(); + + // No-op if already at target + if (targetId === oldLeafId) { + return { cancelled: false }; + } + + // Model required for summarization + if (options.summarize && !this.model) { + throw new Error("No model available for summarization"); + } + + const targetEntry = this.sessionManager.getEntry(targetId); + if (!targetEntry) { + throw new Error(`Entry ${targetId} not found`); + } + + // Collect entries to summarize (from old leaf to common ancestor) + const { entries: entriesToSummarize, commonAncestorId } = collectEntriesForBranchSummary( + this.sessionManager, + oldLeafId, + targetId, + ); + + // Prepare event data - mutable so extensions can override + let customInstructions = options.customInstructions; + let replaceInstructions = options.replaceInstructions; + let label = options.label; + + const preparation: TreePreparation = { + targetId, + oldLeafId, + commonAncestorId, + entriesToSummarize, + userWantsSummary: options.summarize ?? false, + customInstructions, + replaceInstructions, + label, + }; + + // Set up abort controller for summarization + this.branchSummaryAbortController = new AbortController(); + + try { + let extensionSummary: { summary: string; details?: unknown } | undefined; + let fromExtension = false; + + // Emit session_before_tree event + if (this.currentExtensionRunner.hasHandlers("session_before_tree")) { + const result = await this.currentExtensionRunner.emit({ + type: "session_before_tree", + preparation, + signal: this.branchSummaryAbortController.signal, + }); + + if (result?.cancel) { + return { cancelled: true }; + } + + if (result?.summary && options.summarize) { + extensionSummary = result.summary; + fromExtension = true; + } + + // Allow extensions to override instructions and label + if (result?.customInstructions !== undefined) { + customInstructions = result.customInstructions; + } + if (result?.replaceInstructions !== undefined) { + replaceInstructions = result.replaceInstructions; + } + if (result?.label !== undefined) { + label = result.label; + } + } + + // Run default summarizer if needed + let summaryText: string | undefined; + let summaryDetails: unknown; + if (options.summarize && entriesToSummarize.length > 0 && !extensionSummary) { + const model = this.model!; + const { apiKey, headers } = await this.getRequiredRequestAuth(model); + const branchSummarySettings = this.settingsManager.getBranchSummarySettings(); + const result = await generateBranchSummary(entriesToSummarize, { + model, + apiKey, + headers, + signal: this.branchSummaryAbortController.signal, + customInstructions, + replaceInstructions, + reserveTokens: branchSummarySettings.reserveTokens, + }); + if (result.aborted) { + return { cancelled: true, aborted: true }; + } + if (result.error) { + throw new Error(result.error); + } + summaryText = result.summary; + summaryDetails = { + readFiles: result.readFiles || [], + modifiedFiles: result.modifiedFiles || [], + }; + } else if (extensionSummary) { + summaryText = extensionSummary.summary; + summaryDetails = extensionSummary.details; + } + + // Determine the new leaf position based on target type + let newLeafId: string | null; + let editorText: string | undefined; + + if (targetEntry.type === "message" && targetEntry.message.role === "user") { + // User message: leaf = parent (null if root), text goes to editor + newLeafId = targetEntry.parentId; + editorText = this.extractUserMessageText(targetEntry.message.content); + } else if (targetEntry.type === "custom_message") { + // Custom message: leaf = parent (null if root), text goes to editor + newLeafId = targetEntry.parentId; + editorText = + typeof targetEntry.content === "string" + ? targetEntry.content + : targetEntry.content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join(""); + } else { + // Non-user message: leaf = selected node + newLeafId = targetId; + } + + // Switch leaf (with or without summary) + // Summary is attached at the navigation target position (newLeafId), not the old branch + let summaryEntry: BranchSummaryEntry | undefined; + if (summaryText) { + // Create summary at target position (can be null for root) + const summaryId = this.sessionManager.branchWithSummary( + newLeafId, + summaryText, + summaryDetails, + fromExtension, + ); + summaryEntry = this.sessionManager.getEntry(summaryId) as BranchSummaryEntry; + + // Attach label to the summary entry + if (label) { + this.sessionManager.appendLabelChange(summaryId, label); + } + } else if (newLeafId === null) { + // No summary, navigating to root - reset leaf + this.sessionManager.resetLeaf(); + } else { + // No summary, navigating to non-root + this.sessionManager.branch(newLeafId); + } + + // Attach label to target entry when not summarizing (no summary entry to label) + if (label && !summaryText) { + this.sessionManager.appendLabelChange(targetId, label); + } + + // Update agent state + const sessionContext = this.sessionManager.buildSessionContext(); + this.agent.state.messages = sessionContext.messages; + + // Emit session_tree event + await this.currentExtensionRunner.emit({ + type: "session_tree", + newLeafId: this.sessionManager.getLeafId(), + oldLeafId, + summaryEntry, + fromExtension: summaryText ? fromExtension : undefined, + }); + + // Emit to custom tools + + return { editorText, cancelled: false, summaryEntry }; + } finally { + this.branchSummaryAbortController = undefined; + } + } + + /** + * Get all user messages from session for fork selector. + */ + getUserMessagesForForking(): Array<{ entryId: string; text: string }> { + const entries = this.sessionManager.getEntries(); + const result: Array<{ entryId: string; text: string }> = []; + + for (const entry of entries) { + if (entry.type !== "message") { + continue; + } + if (entry.message.role !== "user") { + continue; + } + + const text = this.extractUserMessageText(entry.message.content); + if (text) { + result.push({ entryId: entry.id, text }); + } + } + + return result; + } + + private extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string { + if (typeof content === "string") { + return content; + } + if (Array.isArray(content)) { + return content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join(""); + } + return ""; + } + + /** + * Get session statistics. + */ + getSessionStats(): SessionStats { + const state = this.state; + const userMessages = state.messages.filter((m) => m.role === "user").length; + const assistantMessages = state.messages.filter((m) => m.role === "assistant").length; + const toolResults = state.messages.filter((m) => m.role === "toolResult").length; + + let toolCalls = 0; + let totalInput = 0; + let totalOutput = 0; + let totalCacheRead = 0; + let totalCacheWrite = 0; + let totalCost = 0; + + for (const message of state.messages) { + if (message.role === "assistant") { + const assistantMsg = message; + toolCalls += assistantMsg.content.filter((c) => c.type === "toolCall").length; + totalInput += assistantMsg.usage.input; + totalOutput += assistantMsg.usage.output; + totalCacheRead += assistantMsg.usage.cacheRead; + totalCacheWrite += assistantMsg.usage.cacheWrite; + totalCost += assistantMsg.usage.cost.total; + } + } + + return { + sessionFile: this.sessionFile, + sessionId: this.sessionId, + userMessages, + assistantMessages, + toolCalls, + toolResults, + totalMessages: state.messages.length, + tokens: { + input: totalInput, + output: totalOutput, + cacheRead: totalCacheRead, + cacheWrite: totalCacheWrite, + total: totalInput + totalOutput + totalCacheRead + totalCacheWrite, + }, + cost: totalCost, + contextUsage: this.getContextUsage(), + }; + } + + getContextUsage(): ContextUsage | undefined { + const model = this.model; + if (!model) { + return undefined; + } + + const contextWindow = model.contextWindow ?? 0; + if (contextWindow <= 0) { + return undefined; + } + + // After compaction, the last assistant usage reflects pre-compaction context size. + // We can only trust usage from an assistant that responded after the latest compaction. + // If no such assistant exists, context token count is unknown until the next LLM response. + const branchEntries = this.sessionManager.getBranch(); + const latestCompaction = getLatestCompactionEntry(branchEntries); + + if (latestCompaction) { + // Check if there's a valid assistant usage after the compaction boundary + const compactionIndex = branchEntries.lastIndexOf(latestCompaction); + let hasPostCompactionUsage = false; + for (let i = branchEntries.length - 1; i > compactionIndex; i--) { + const entry = branchEntries[i]; + if (entry.type === "message" && entry.message.role === "assistant") { + const assistant = entry.message; + if (assistant.stopReason !== "aborted" && assistant.stopReason !== "error") { + const contextTokens = calculateContextTokens(assistant.usage); + if (contextTokens > 0) { + hasPostCompactionUsage = true; + } + break; + } + } + } + + if (!hasPostCompactionUsage) { + return { tokens: null, contextWindow, percent: null }; + } + } + + const estimate = estimateContextTokens(this.messages); + const percent = (estimate.tokens / contextWindow) * 100; + + return { + tokens: estimate.tokens, + contextWindow, + percent, + }; + } + + /** + * @deprecated Use the OpenClaw session export command instead. + * @param outputPath Optional output path (defaults to session directory) + * @returns Path to exported file + */ + async exportToHtml(_outputPath?: string): Promise { + throw new Error( + "AgentSession.exportToHtml is deprecated; use the OpenClaw session export command.", + ); + } + + /** + * Export the current session branch to a JSONL file. + * Writes the session header followed by all entries on the current branch path. + * @param outputPath Target file path. If omitted, generates a timestamped file in cwd. + * @returns The resolved output file path. + */ + exportToJsonl(outputPath?: string): string { + const filePath = resolve( + outputPath ?? `session-${new Date().toISOString().replace(/[:.]/g, "-")}.jsonl`, + ); + const dir = dirname(filePath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + const header: SessionHeader = { + type: "session", + version: CURRENT_SESSION_VERSION, + id: this.sessionManager.getSessionId(), + timestamp: new Date().toISOString(), + cwd: this.sessionManager.getCwd(), + }; + + const branchEntries = this.sessionManager.getBranch(); + const lines = [JSON.stringify(header)]; + + // Re-chain parentIds to form a linear sequence + let prevId: string | null = null; + for (const entry of branchEntries) { + const linear = { ...entry, parentId: prevId }; + lines.push(JSON.stringify(linear)); + prevId = entry.id; + } + + writeFileSync(filePath, `${lines.join("\n")}\n`); + return filePath; + } + + // ========================================================================= + // Utilities + // ========================================================================= + + /** + * Get text content of last assistant message. + * Useful for /copy command. + * @returns Text content, or undefined if no assistant message exists + */ + getLastAssistantText(): string | undefined { + const lastAssistant = this.messages + .slice() + .toReversed() + .find((m) => { + if (m.role !== "assistant") { + return false; + } + const msg = m; + // Skip aborted messages with no content + if (msg.stopReason === "aborted" && msg.content.length === 0) { + return false; + } + return true; + }); + + if (!lastAssistant) { + return undefined; + } + + let text = ""; + for (const content of (lastAssistant as AssistantMessage).content) { + if (content.type === "text") { + text += content.text; + } + } + + return text.trim() || undefined; + } + + // ========================================================================= + // Extension System + // ========================================================================= + + createReplacedSessionContext(): ReplacedSessionContext { + const context = Object.defineProperties( + {}, + Object.getOwnPropertyDescriptors(this.currentExtensionRunner.createCommandContext()), + ) as ReplacedSessionContext; + context.sendMessage = (message, options) => this.sendCustomMessage(message, options); + context.sendUserMessage = (content, options) => this.sendUserMessage(content, options); + return context; + } + + /** + * Check if extensions have handlers for a specific event type. + */ + hasExtensionHandlers(eventType: string): boolean { + return this.currentExtensionRunner.hasHandlers(eventType); + } + + /** + * Get the extension runner (for setting UI context and error handlers). + */ + get extensionRunner(): ExtensionRunner { + return this.currentExtensionRunner; + } +} diff --git a/src/agents/sessions/auth-guidance.ts b/src/agents/sessions/auth-guidance.ts new file mode 100644 index 00000000000..31049c76ede --- /dev/null +++ b/src/agents/sessions/auth-guidance.ts @@ -0,0 +1,25 @@ +import { join } from "node:path"; +import { getDocsPath } from "../config.js"; + +const UNKNOWN_PROVIDER = "unknown"; + +export function getProviderLoginHelp(): string { + return [ + "Use /login to log into a provider via OAuth or API key. See:", + ` ${join(getDocsPath(), "providers.md")}`, + ` ${join(getDocsPath(), "models.md")}`, + ].join("\n"); +} + +export function formatNoModelsAvailableMessage(): string { + return `No models available. ${getProviderLoginHelp()}`; +} + +export function formatNoModelSelectedMessage(): string { + return `No model selected.\n\n${getProviderLoginHelp()}\n\nThen use /model to select a model.`; +} + +export function formatNoApiKeyFoundMessage(provider: string): string { + const providerDisplay = provider === UNKNOWN_PROVIDER ? "the selected model" : provider; + return `No API key found for ${providerDisplay}.\n\n${getProviderLoginHelp()}`; +} diff --git a/src/agents/sessions/auth-storage.ts b/src/agents/sessions/auth-storage.ts new file mode 100644 index 00000000000..c58e5cfc3f1 --- /dev/null +++ b/src/agents/sessions/auth-storage.ts @@ -0,0 +1,551 @@ +/** + * Credential storage for API keys and OAuth tokens. + * Handles loading, saving, and refreshing credentials from auth.json. + * + * Uses file locking to prevent race conditions when multiple agent sessions + * try to refresh tokens simultaneously. + */ + +import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { + findEnvKeys, + getEnvApiKey, + type OAuthCredentials, + type OAuthLoginCallbacks, + type OAuthProviderId, +} from "openclaw/plugin-sdk/llm"; +import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "openclaw/plugin-sdk/llm-oauth"; +import lockfile from "proper-lockfile"; +import { getAgentDir } from "../config.js"; +import { resolveConfigValue } from "./resolve-config-value.js"; + +export type ApiKeyCredential = { + type: "api_key"; + key: string; +}; + +export type OAuthCredential = { + type: "oauth"; +} & OAuthCredentials; + +export type AuthCredential = ApiKeyCredential | OAuthCredential; + +export type AuthStorageData = Record; + +export type AuthStatus = { + configured: boolean; + source?: + | "stored" + | "runtime" + | "environment" + | "fallback" + | "models_json_key" + | "models_json_command"; + label?: string; +}; + +type LockResult = { + result: T; + next?: string; +}; + +export interface AuthStorageBackend { + withLock(fn: (current: string | undefined) => LockResult): T; + withLockAsync(fn: (current: string | undefined) => Promise>): Promise; +} + +export class FileAuthStorageBackend implements AuthStorageBackend { + private authPath: string; + + constructor(authPath: string = join(getAgentDir(), "auth.json")) { + this.authPath = authPath; + } + + private ensureParentDir(): void { + const dir = dirname(this.authPath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true, mode: 0o700 }); + } + } + + private ensureFileExists(): void { + if (!existsSync(this.authPath)) { + writeFileSync(this.authPath, "{}", "utf-8"); + chmodSync(this.authPath, 0o600); + } + } + + private acquireLockSyncWithRetry(path: string): () => void { + const maxAttempts = 10; + const delayMs = 20; + let lastError: unknown; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return lockfile.lockSync(path, { realpath: false }); + } catch (error) { + const code = + typeof error === "object" && error !== null && "code" in error + ? String((error as { code?: unknown }).code) + : undefined; + if (code !== "ELOCKED" || attempt === maxAttempts) { + throw error; + } + lastError = error; + const start = Date.now(); + while (Date.now() - start < delayMs) { + // Sleep synchronously to avoid changing callers to async. + } + } + } + + throw (lastError as Error) ?? new Error("Failed to acquire auth storage lock"); + } + + withLock(fn: (current: string | undefined) => LockResult): T { + this.ensureParentDir(); + this.ensureFileExists(); + + let release: (() => void) | undefined; + try { + release = this.acquireLockSyncWithRetry(this.authPath); + const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined; + const { result, next } = fn(current); + if (next !== undefined) { + writeFileSync(this.authPath, next, "utf-8"); + chmodSync(this.authPath, 0o600); + } + return result; + } finally { + if (release) { + release(); + } + } + } + + async withLockAsync(fn: (current: string | undefined) => Promise>): Promise { + this.ensureParentDir(); + this.ensureFileExists(); + + let release: (() => Promise) | undefined; + let lockCompromised = false; + let lockCompromisedError: Error | undefined; + const throwIfCompromised = () => { + if (lockCompromised) { + throw lockCompromisedError ?? new Error("Auth storage lock was compromised"); + } + }; + + try { + release = await lockfile.lock(this.authPath, { + retries: { + retries: 10, + factor: 2, + minTimeout: 100, + maxTimeout: 10000, + randomize: true, + }, + stale: 30000, + onCompromised: (err) => { + lockCompromised = true; + lockCompromisedError = err; + }, + }); + + throwIfCompromised(); + const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined; + const { result, next } = await fn(current); + throwIfCompromised(); + if (next !== undefined) { + writeFileSync(this.authPath, next, "utf-8"); + chmodSync(this.authPath, 0o600); + } + throwIfCompromised(); + return result; + } finally { + if (release) { + try { + await release(); + } catch { + // Ignore unlock errors when lock is compromised. + } + } + } + } +} + +export class InMemoryAuthStorageBackend implements AuthStorageBackend { + private value: string | undefined; + + withLock(fn: (current: string | undefined) => LockResult): T { + const { result, next } = fn(this.value); + if (next !== undefined) { + this.value = next; + } + return result; + } + + async withLockAsync(fn: (current: string | undefined) => Promise>): Promise { + const { result, next } = await fn(this.value); + if (next !== undefined) { + this.value = next; + } + return result; + } +} + +/** + * Credential storage backed by a JSON file. + */ +export class AuthStorage { + private data: AuthStorageData = {}; + private runtimeOverrides: Map = new Map(); + private fallbackResolver?: (provider: string) => string | undefined; + private loadError: Error | null = null; + private errors: Error[] = []; + private storage: AuthStorageBackend; + + private constructor(storage: AuthStorageBackend) { + this.storage = storage; + this.reload(); + } + + static create(authPath?: string): AuthStorage { + return new AuthStorage( + new FileAuthStorageBackend(authPath ?? join(getAgentDir(), "auth.json")), + ); + } + + static fromStorage(storage: AuthStorageBackend): AuthStorage { + return new AuthStorage(storage); + } + + static inMemory(data: AuthStorageData = {}): AuthStorage { + const storage = new InMemoryAuthStorageBackend(); + storage.withLock(() => ({ result: undefined, next: JSON.stringify(data, null, 2) })); + return AuthStorage.fromStorage(storage); + } + + /** + * Set a runtime API key override (not persisted to disk). + * Used for CLI --api-key flag. + */ + setRuntimeApiKey(provider: string, apiKey: string): void { + this.runtimeOverrides.set(provider, apiKey); + } + + /** + * Remove a runtime API key override. + */ + removeRuntimeApiKey(provider: string): void { + this.runtimeOverrides.delete(provider); + } + + /** + * Set a fallback resolver for API keys not found in auth.json or env vars. + * Used for custom provider keys from models.json. + */ + setFallbackResolver(resolver: (provider: string) => string | undefined): void { + this.fallbackResolver = resolver; + } + + private recordError(error: unknown): void { + const normalizedError = error instanceof Error ? error : new Error(String(error)); + this.errors.push(normalizedError); + } + + private parseStorageData(content: string | undefined): AuthStorageData { + if (!content) { + return {}; + } + return JSON.parse(content) as AuthStorageData; + } + + /** + * Reload credentials from storage. + */ + reload(): void { + let content: string | undefined; + try { + this.storage.withLock((current) => { + content = current; + return { result: undefined }; + }); + this.data = this.parseStorageData(content); + this.loadError = null; + } catch (error) { + this.loadError = error as Error; + this.recordError(error); + } + } + + private persistProviderChange(provider: string, credential: AuthCredential | undefined): void { + if (this.loadError) { + return; + } + + try { + this.storage.withLock((current) => { + const currentData = this.parseStorageData(current); + const merged: AuthStorageData = { ...currentData }; + if (credential) { + merged[provider] = credential; + } else { + delete merged[provider]; + } + return { result: undefined, next: JSON.stringify(merged, null, 2) }; + }); + } catch (error) { + this.recordError(error); + } + } + + /** + * Get credential for a provider. + */ + get(provider: string): AuthCredential | undefined { + return this.data[provider] ?? undefined; + } + + /** + * Set credential for a provider. + */ + set(provider: string, credential: AuthCredential): void { + this.data[provider] = credential; + this.persistProviderChange(provider, credential); + } + + /** + * Remove credential for a provider. + */ + remove(provider: string): void { + delete this.data[provider]; + this.persistProviderChange(provider, undefined); + } + + /** + * List all providers with credentials. + */ + list(): string[] { + return Object.keys(this.data); + } + + /** + * Check if credentials exist for a provider in auth.json. + */ + has(provider: string): boolean { + return provider in this.data; + } + + /** + * Check if any form of auth is configured for a provider. + * Unlike getApiKey(), this doesn't refresh OAuth tokens. + */ + hasAuth(provider: string): boolean { + if (this.runtimeOverrides.has(provider)) { + return true; + } + if (this.data[provider]) { + return true; + } + if (getEnvApiKey(provider)) { + return true; + } + if (this.fallbackResolver?.(provider)) { + return true; + } + return false; + } + + /** + * Return auth status without exposing credential values or refreshing tokens. + */ + getAuthStatus(provider: string): AuthStatus { + if (this.data[provider]) { + return { configured: true, source: "stored" }; + } + + if (this.runtimeOverrides.has(provider)) { + return { configured: false, source: "runtime", label: "--api-key" }; + } + + const envKeys = findEnvKeys(provider); + if (envKeys?.[0]) { + return { configured: false, source: "environment", label: envKeys[0] }; + } + + if (this.fallbackResolver?.(provider)) { + return { configured: false, source: "fallback", label: "custom provider config" }; + } + + return { configured: false }; + } + + /** + * Get all credentials (for passing to getOAuthApiKey). + */ + getAll(): AuthStorageData { + return { ...this.data }; + } + + drainErrors(): Error[] { + const drained = [...this.errors]; + this.errors = []; + return drained; + } + + /** + * Login to an OAuth provider. + */ + async login(providerId: OAuthProviderId, callbacks: OAuthLoginCallbacks): Promise { + const provider = getOAuthProvider(providerId); + if (!provider) { + throw new Error(`Unknown OAuth provider: ${providerId}`); + } + + const credentials = await provider.login(callbacks); + this.set(providerId, { type: "oauth", ...credentials }); + } + + /** + * Logout from a provider. + */ + logout(provider: string): void { + this.remove(provider); + } + + /** + * Refresh OAuth token with backend locking to prevent race conditions. + * Multiple agent sessions may try to refresh simultaneously when tokens expire. + */ + private async refreshOAuthTokenWithLock( + providerId: OAuthProviderId, + ): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> { + const provider = getOAuthProvider(providerId); + if (!provider) { + return null; + } + + const result = await this.storage.withLockAsync(async (current) => { + const currentData = this.parseStorageData(current); + this.data = currentData; + this.loadError = null; + + const cred = currentData[providerId]; + if (cred?.type !== "oauth") { + return { result: null }; + } + + if (Date.now() < cred.expires) { + return { result: { apiKey: provider.getApiKey(cred), newCredentials: cred } }; + } + + const oauthCreds: Record = {}; + for (const [key, value] of Object.entries(currentData)) { + if (value.type === "oauth") { + oauthCreds[key] = value; + } + } + + const refreshed = await getOAuthApiKey(providerId, oauthCreds); + if (!refreshed) { + return { result: null }; + } + + const merged: AuthStorageData = { + ...currentData, + [providerId]: { type: "oauth", ...refreshed.newCredentials }, + }; + this.data = merged; + this.loadError = null; + return { result: refreshed, next: JSON.stringify(merged, null, 2) }; + }); + + return result; + } + + /** + * Get API key for a provider. + * Priority: + * 1. Runtime override (CLI --api-key) + * 2. API key from auth.json + * 3. OAuth token from auth.json (auto-refreshed with locking) + * 4. Environment variable + * 5. Fallback resolver (models.json custom providers) + */ + async getApiKey( + providerId: string, + options?: { includeFallback?: boolean }, + ): Promise { + // Runtime override takes highest priority + const runtimeKey = this.runtimeOverrides.get(providerId); + if (runtimeKey) { + return runtimeKey; + } + + const cred = this.data[providerId]; + + if (cred?.type === "api_key") { + return resolveConfigValue(cred.key); + } + + if (cred?.type === "oauth") { + const provider = getOAuthProvider(providerId); + if (!provider) { + // Unknown OAuth provider, can't get API key + return undefined; + } + + // Check if token needs refresh + const needsRefresh = Date.now() >= cred.expires; + + if (needsRefresh) { + // Use locked refresh to prevent race conditions + try { + const result = await this.refreshOAuthTokenWithLock(providerId); + if (result) { + return result.apiKey; + } + } catch (error) { + this.recordError(error); + // Refresh failed - re-read file to check if another instance succeeded + this.reload(); + const updatedCred = this.data[providerId]; + + if (updatedCred?.type === "oauth" && Date.now() < updatedCred.expires) { + // Another instance refreshed successfully, use those credentials + return provider.getApiKey(updatedCred); + } + + // Refresh truly failed - return undefined so model discovery skips this provider + // User can /login to re-authenticate (credentials preserved for retry) + return undefined; + } + } else { + // Token not expired, use current access token + return provider.getApiKey(cred); + } + } + + // Fall back to environment variable + const envKey = getEnvApiKey(providerId); + if (envKey) { + return envKey; + } + + // Fall back to custom resolver (e.g., models.json custom providers) + if (options?.includeFallback !== false) { + return this.fallbackResolver?.(providerId) ?? undefined; + } + + return undefined; + } + + /** + * Get all registered OAuth providers + */ + getOAuthProviders() { + return getOAuthProviders(); + } +} diff --git a/src/agents/sessions/bash-executor.ts b/src/agents/sessions/bash-executor.ts new file mode 100644 index 00000000000..b2e5e28944b --- /dev/null +++ b/src/agents/sessions/bash-executor.ts @@ -0,0 +1,159 @@ +/** + * Bash command execution with streaming support and cancellation. + * + * This module provides a unified bash execution implementation used by: + * - AgentSession.executeBash() for interactive and RPC modes + * - Direct calls from modes that need bash execution + */ + +import { randomBytes } from "node:crypto"; +import { createWriteStream, type WriteStream } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { stripAnsi } from "../utils/ansi.js"; +import { sanitizeBinaryOutput } from "../utils/shell.js"; +import type { BashOperations } from "./tools/bash.js"; +import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate.js"; + +// ============================================================================ +// Types +// ============================================================================ + +export interface BashExecutorOptions { + /** Callback for streaming output chunks (already sanitized) */ + onChunk?: (chunk: string) => void; + /** AbortSignal for cancellation */ + signal?: AbortSignal; +} + +export interface BashResult { + /** Combined stdout + stderr output (sanitized, possibly truncated) */ + output: string; + /** Process exit code (undefined if killed/cancelled) */ + exitCode: number | undefined; + /** Whether the command was cancelled via signal */ + cancelled: boolean; + /** Whether the output was truncated */ + truncated: boolean; + /** Path to temp file containing full output (if output exceeded truncation threshold) */ + fullOutputPath?: string; +} + +// ============================================================================ +// Implementation +// ============================================================================ + +/** + * Execute a bash command using custom BashOperations. + * Used for remote execution (SSH, containers, etc.). + */ +export async function executeBashWithOperations( + command: string, + cwd: string, + operations: BashOperations, + options?: BashExecutorOptions, +): Promise { + const outputChunks: string[] = []; + let outputBytes = 0; + const maxOutputBytes = DEFAULT_MAX_BYTES * 2; + + let tempFilePath: string | undefined; + let tempFileStream: WriteStream | undefined; + let totalBytes = 0; + + const ensureTempFile = () => { + if (tempFilePath) { + return; + } + const id = randomBytes(8).toString("hex"); + tempFilePath = join(tmpdir(), `openclaw-bash-${id}.log`); + tempFileStream = createWriteStream(tempFilePath); + for (const chunk of outputChunks) { + tempFileStream.write(chunk); + } + }; + + const decoder = new TextDecoder(); + + const onData = (data: Buffer) => { + totalBytes += data.length; + + // Sanitize: strip ANSI, replace binary garbage, normalize newlines + const text = sanitizeBinaryOutput(stripAnsi(decoder.decode(data, { stream: true }))).replace( + /\r/g, + "", + ); + + // Start writing to temp file if exceeds threshold + if (totalBytes > DEFAULT_MAX_BYTES) { + ensureTempFile(); + } + + if (tempFileStream) { + tempFileStream.write(text); + } + + // Keep rolling buffer + outputChunks.push(text); + outputBytes += text.length; + while (outputBytes > maxOutputBytes && outputChunks.length > 1) { + const removed = outputChunks.shift()!; + outputBytes -= removed.length; + } + + // Stream to callback + if (options?.onChunk) { + options.onChunk(text); + } + }; + + try { + const result = await operations.exec(command, cwd, { + onData, + signal: options?.signal, + }); + + const fullOutput = outputChunks.join(""); + const truncationResult = truncateTail(fullOutput); + if (truncationResult.truncated) { + ensureTempFile(); + } + if (tempFileStream) { + tempFileStream.end(); + } + const cancelled = options?.signal?.aborted ?? false; + + return { + output: truncationResult.truncated ? truncationResult.content : fullOutput, + exitCode: cancelled ? undefined : (result.exitCode ?? undefined), + cancelled, + truncated: truncationResult.truncated, + fullOutputPath: tempFilePath, + }; + } catch (err) { + // Check if it was an abort + if (options?.signal?.aborted) { + const fullOutput = outputChunks.join(""); + const truncationResult = truncateTail(fullOutput); + if (truncationResult.truncated) { + ensureTempFile(); + } + if (tempFileStream) { + tempFileStream.end(); + } + return { + output: truncationResult.truncated ? truncationResult.content : fullOutput, + exitCode: undefined, + cancelled: true, + truncated: truncationResult.truncated, + fullOutputPath: tempFilePath, + }; + } + + if (tempFileStream) { + tempFileStream.end(); + } + + throw err; + } +} diff --git a/src/agents/sessions/compaction/branch-summarization.ts b/src/agents/sessions/compaction/branch-summarization.ts new file mode 100644 index 00000000000..a766c27ea4f --- /dev/null +++ b/src/agents/sessions/compaction/branch-summarization.ts @@ -0,0 +1,381 @@ +/** + * Branch summarization for tree navigation. + * + * When navigating to a different point in the session tree, this generates + * a summary of the branch being left so context isn't lost. + */ + +import type { Model } from "openclaw/plugin-sdk/llm"; +import { completeSimple } from "openclaw/plugin-sdk/llm"; +import type { AgentMessage } from "../../runtime/index.js"; +import { + convertToLlm, + createBranchSummaryMessage, + createCompactionSummaryMessage, + createCustomMessage, +} from "../messages.js"; +import type { ReadonlySessionManager, SessionEntry } from "../session-manager.js"; +import { estimateTokens } from "./compaction.js"; +import { + computeFileLists, + createFileOps, + extractFileOpsFromMessage, + type FileOperations, + formatFileOperations, + SUMMARIZATION_SYSTEM_PROMPT, + serializeConversation, +} from "./utils.js"; + +// ============================================================================ +// Types +// ============================================================================ + +export interface BranchSummaryResult { + summary?: string; + readFiles?: string[]; + modifiedFiles?: string[]; + aborted?: boolean; + error?: string; +} + +/** Details stored in BranchSummaryEntry.details for file tracking */ +export interface BranchSummaryDetails { + readFiles: string[]; + modifiedFiles: string[]; +} + +export type { FileOperations } from "./utils.js"; + +export interface BranchPreparation { + /** Messages extracted for summarization, in chronological order */ + messages: AgentMessage[]; + /** File operations extracted from tool calls */ + fileOps: FileOperations; + /** Total estimated tokens in messages */ + totalTokens: number; +} + +export interface CollectEntriesResult { + /** Entries to summarize, in chronological order */ + entries: SessionEntry[]; + /** Common ancestor between old and new position, if any */ + commonAncestorId: string | null; +} + +export interface GenerateBranchSummaryOptions { + /** Model to use for summarization */ + model: Model; + /** API key for the model */ + apiKey: string; + /** Request headers for the model */ + headers?: Record; + /** Abort signal for cancellation */ + signal: AbortSignal; + /** Optional custom instructions for summarization */ + customInstructions?: string; + /** If true, customInstructions replaces the default prompt instead of being appended */ + replaceInstructions?: boolean; + /** Tokens reserved for prompt + LLM response (default 16384) */ + reserveTokens?: number; +} + +// ============================================================================ +// Entry Collection +// ============================================================================ + +/** + * Collect entries that should be summarized when navigating from one position to another. + * + * Walks from oldLeafId back to the common ancestor with targetId, collecting entries + * along the way. Does NOT stop at compaction boundaries - those are included and their + * summaries become context. + * + * @param session - Session manager (read-only access) + * @param oldLeafId - Current position (where we're navigating from) + * @param targetId - Target position (where we're navigating to) + * @returns Entries to summarize and the common ancestor + */ +export function collectEntriesForBranchSummary( + session: ReadonlySessionManager, + oldLeafId: string | null, + targetId: string, +): CollectEntriesResult { + // If no old position, nothing to summarize + if (!oldLeafId) { + return { entries: [], commonAncestorId: null }; + } + + // Find common ancestor (deepest node that's on both paths) + const oldPath = new Set(session.getBranch(oldLeafId).map((e) => e.id)); + const targetPath = session.getBranch(targetId); + + // targetPath is root-first, so iterate backwards to find deepest common ancestor + let commonAncestorId: string | null = null; + for (let i = targetPath.length - 1; i >= 0; i--) { + if (oldPath.has(targetPath[i].id)) { + commonAncestorId = targetPath[i].id; + break; + } + } + + // Collect entries from old leaf back to common ancestor + const entries: SessionEntry[] = []; + let current: string | null = oldLeafId; + + while (current && current !== commonAncestorId) { + const entry = session.getEntry(current); + if (!entry) { + break; + } + entries.push(entry); + current = entry.parentId; + } + + // Reverse to get chronological order + entries.reverse(); + + return { entries, commonAncestorId }; +} + +// ============================================================================ +// Entry to Message Conversion +// ============================================================================ + +/** + * Extract AgentMessage from a session entry. + * Similar to getMessageFromEntry in compaction.ts but also handles compaction entries. + */ +function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined { + switch (entry.type) { + case "message": + // Skip tool results - context is in assistant's tool call + if (entry.message.role === "toolResult") { + return undefined; + } + return entry.message; + + case "custom_message": + return createCustomMessage( + entry.customType, + entry.content, + entry.display, + entry.details, + entry.timestamp, + ); + + case "branch_summary": + return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp); + + case "compaction": + return createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp); + + // These don't contribute to conversation content + case "thinking_level_change": + case "model_change": + case "custom": + case "label": + case "session_info": + return undefined; + } + return undefined; +} + +/** + * Prepare entries for summarization with token budget. + * + * Walks entries from NEWEST to OLDEST, adding messages until we hit the token budget. + * This ensures we keep the most recent context when the branch is too long. + * + * Also collects file operations from: + * - Tool calls in assistant messages + * - Existing branch_summary entries' details (for cumulative tracking) + * + * @param entries - Entries in chronological order + * @param tokenBudget - Maximum tokens to include (0 = no limit) + */ +export function prepareBranchEntries( + entries: SessionEntry[], + tokenBudget: number = 0, +): BranchPreparation { + const messages: AgentMessage[] = []; + const fileOps = createFileOps(); + let totalTokens = 0; + + // First pass: collect file ops from ALL entries (even if they don't fit in token budget) + // This ensures we capture cumulative file tracking from nested branch summaries + // Only extract from runtime-generated summaries (fromHook !== true), not extension-generated ones + for (const entry of entries) { + if (entry.type === "branch_summary" && !entry.fromHook && entry.details) { + const details = entry.details as BranchSummaryDetails; + if (Array.isArray(details.readFiles)) { + for (const f of details.readFiles) { + fileOps.read.add(f); + } + } + if (Array.isArray(details.modifiedFiles)) { + // Modified files go into both edited and written for proper deduplication + for (const f of details.modifiedFiles) { + fileOps.edited.add(f); + } + } + } + } + + // Second pass: walk from newest to oldest, adding messages until token budget + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i]; + const message = getMessageFromEntry(entry); + if (!message) { + continue; + } + + // Extract file ops from assistant messages (tool calls) + extractFileOpsFromMessage(message, fileOps); + + const tokens = estimateTokens(message); + + // Check budget before adding + if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) { + // If this is a summary entry, try to fit it anyway as it's important context + if (entry.type === "compaction" || entry.type === "branch_summary") { + if (totalTokens < tokenBudget * 0.9) { + messages.unshift(message); + totalTokens += tokens; + } + } + // Stop - we've hit the budget + break; + } + + messages.unshift(message); + totalTokens += tokens; + } + + return { messages, fileOps, totalTokens }; +} + +// ============================================================================ +// Summary Generation +// ============================================================================ + +const BRANCH_SUMMARY_PREAMBLE = `The user explored a different conversation branch before returning here. +Summary of that exploration: + +`; + +const BRANCH_SUMMARY_PROMPT = `Create a structured summary of this conversation branch for context when returning later. + +Use this EXACT format: + +## Goal +[What was the user trying to accomplish in this branch?] + +## Constraints & Preferences +- [Any constraints, preferences, or requirements mentioned] +- [Or "(none)" if none were mentioned] + +## Progress +### Done +- [x] [Completed tasks/changes] + +### In Progress +- [ ] [Work that was started but not finished] + +### Blocked +- [Issues preventing progress, if any] + +## Key Decisions +- **[Decision]**: [Brief rationale] + +## Next Steps +1. [What should happen next to continue this work] + +Keep each section concise. Preserve exact file paths, function names, and error messages.`; + +/** + * Generate a summary of abandoned branch entries. + * + * @param entries - Session entries to summarize (chronological order) + * @param options - Generation options + */ +export async function generateBranchSummary( + entries: SessionEntry[], + options: GenerateBranchSummaryOptions, +): Promise { + const { + model, + apiKey, + headers, + signal, + customInstructions, + replaceInstructions, + reserveTokens = 16384, + } = options; + + // Token budget = context window minus reserved space for prompt + response + const contextWindow = model.contextWindow || 128000; + const tokenBudget = contextWindow - reserveTokens; + + const { messages, fileOps } = prepareBranchEntries(entries, tokenBudget); + + if (messages.length === 0) { + return { summary: "No content to summarize" }; + } + + // Transform to LLM-compatible messages, then serialize to text + // Serialization prevents the model from treating it as a conversation to continue + const llmMessages = convertToLlm(messages); + const conversationText = serializeConversation(llmMessages); + + // Build prompt + let instructions: string; + if (replaceInstructions && customInstructions) { + instructions = customInstructions; + } else if (customInstructions) { + instructions = `${BRANCH_SUMMARY_PROMPT}\n\nAdditional focus: ${customInstructions}`; + } else { + instructions = BRANCH_SUMMARY_PROMPT; + } + const promptText = `\n${conversationText}\n\n\n${instructions}`; + + const summarizationMessages = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: promptText }], + timestamp: Date.now(), + }, + ]; + + // Call LLM for summarization + const response = await completeSimple( + model, + { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, + { apiKey, headers, signal, maxTokens: 2048 }, + ); + + // Check if aborted or errored + if (response.stopReason === "aborted") { + return { aborted: true }; + } + if (response.stopReason === "error") { + return { error: response.errorMessage || "Summarization failed" }; + } + + let summary = response.content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join("\n"); + + // Prepend preamble to provide context about the branch summary + summary = BRANCH_SUMMARY_PREAMBLE + summary; + + // Compute file lists and append to summary + const { readFiles, modifiedFiles } = computeFileLists(fileOps); + summary += formatFileOperations(readFiles, modifiedFiles); + + return { + summary: summary || "No summary generated", + readFiles, + modifiedFiles, + }; +} diff --git a/src/agents/sessions/compaction/compaction.ts b/src/agents/sessions/compaction/compaction.ts new file mode 100644 index 00000000000..c19091ab17a --- /dev/null +++ b/src/agents/sessions/compaction/compaction.ts @@ -0,0 +1,938 @@ +/** + * Context compaction for long sessions. + * + * Pure functions for compaction logic. The session manager handles I/O, + * and after compaction the session is reloaded. + */ + +import type { + AssistantMessage, + Context, + Model, + SimpleStreamOptions, + Usage, +} from "openclaw/plugin-sdk/llm"; +import { completeSimple } from "openclaw/plugin-sdk/llm"; +import type { AgentMessage, StreamFn, ThinkingLevel } from "../../runtime/index.js"; +import { + convertToLlm, + createBranchSummaryMessage, + createCompactionSummaryMessage, + createCustomMessage, +} from "../messages.js"; +import { + buildSessionContext, + type CompactionEntry, + type SessionEntry, +} from "../session-manager.js"; +import { + computeFileLists, + createFileOps, + extractFileOpsFromMessage, + type FileOperations, + formatFileOperations, + SUMMARIZATION_SYSTEM_PROMPT, + serializeConversation, +} from "./utils.js"; + +// ============================================================================ +// File Operation Tracking +// ============================================================================ + +/** Details stored in CompactionEntry.details for file tracking */ +export interface CompactionDetails { + readFiles: string[]; + modifiedFiles: string[]; +} + +/** + * Extract file operations from messages and previous compaction entries. + */ +function extractFileOperations( + messages: AgentMessage[], + entries: SessionEntry[], + prevCompactionIndex: number, +): FileOperations { + const fileOps = createFileOps(); + + // Collect from previous compaction's details (if runtime-generated) + if (prevCompactionIndex >= 0) { + const prevCompaction = entries[prevCompactionIndex] as CompactionEntry; + if (!prevCompaction.fromHook && prevCompaction.details) { + // fromHook field kept for session file compatibility + const details = prevCompaction.details as CompactionDetails; + if (Array.isArray(details.readFiles)) { + for (const f of details.readFiles) { + fileOps.read.add(f); + } + } + if (Array.isArray(details.modifiedFiles)) { + for (const f of details.modifiedFiles) { + fileOps.edited.add(f); + } + } + } + } + + // Extract from tool calls in messages + for (const msg of messages) { + extractFileOpsFromMessage(msg, fileOps); + } + + return fileOps; +} + +// ============================================================================ +// Message Extraction +// ============================================================================ + +/** + * Extract AgentMessage from an entry if it produces one. + * Returns undefined for entries that don't contribute to LLM context. + */ +function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined { + if (entry.type === "message") { + return entry.message; + } + if (entry.type === "custom_message") { + return createCustomMessage( + entry.customType, + entry.content, + entry.display, + entry.details, + entry.timestamp, + ); + } + if (entry.type === "branch_summary") { + return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp); + } + if (entry.type === "compaction") { + return createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp); + } + return undefined; +} + +function getMessageFromEntryForCompaction(entry: SessionEntry): AgentMessage | undefined { + if (entry.type === "compaction") { + return undefined; + } + return getMessageFromEntry(entry); +} + +/** Result from compact() - SessionManager adds uuid/parentUuid when saving */ +export interface CompactionResult { + summary: string; + firstKeptEntryId: string; + tokensBefore: number; + /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */ + details?: T; +} + +// ============================================================================ +// Types +// ============================================================================ + +export interface CompactionSettings { + enabled: boolean; + reserveTokens: number; + keepRecentTokens: number; +} + +export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = { + enabled: true, + reserveTokens: 16384, + keepRecentTokens: 20000, +}; + +// ============================================================================ +// Token calculation +// ============================================================================ + +/** + * Calculate total context tokens from usage. + * Uses the native totalTokens field when available, falls back to computing from components. + */ +export function calculateContextTokens(usage: Usage): number { + return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite; +} + +/** + * Get usage from an assistant message if available. + * Skips aborted and error messages as they don't have valid usage data. + */ +function getAssistantUsage(msg: AgentMessage): Usage | undefined { + if (msg.role === "assistant" && "usage" in msg) { + const assistantMsg = msg; + if ( + assistantMsg.stopReason !== "aborted" && + assistantMsg.stopReason !== "error" && + assistantMsg.usage + ) { + return assistantMsg.usage; + } + } + return undefined; +} + +/** + * Find the last non-aborted assistant message usage from session entries. + */ +export function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined { + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i]; + if (entry.type === "message") { + const usage = getAssistantUsage(entry.message); + if (usage) { + return usage; + } + } + } + return undefined; +} + +export interface ContextUsageEstimate { + tokens: number; + usageTokens: number; + trailingTokens: number; + lastUsageIndex: number | null; +} + +function getLastAssistantUsageInfo( + messages: AgentMessage[], +): { usage: Usage; index: number } | undefined { + for (let i = messages.length - 1; i >= 0; i--) { + const usage = getAssistantUsage(messages[i]); + if (usage) { + return { usage, index: i }; + } + } + return undefined; +} + +/** + * Estimate context tokens from messages, using the last assistant usage when available. + * If there are messages after the last usage, estimate their tokens with estimateTokens. + */ +export function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate { + const usageInfo = getLastAssistantUsageInfo(messages); + + if (!usageInfo) { + let estimated = 0; + for (const message of messages) { + estimated += estimateTokens(message); + } + return { + tokens: estimated, + usageTokens: 0, + trailingTokens: estimated, + lastUsageIndex: null, + }; + } + + const usageTokens = calculateContextTokens(usageInfo.usage); + let trailingTokens = 0; + for (let i = usageInfo.index + 1; i < messages.length; i++) { + trailingTokens += estimateTokens(messages[i]); + } + + return { + tokens: usageTokens + trailingTokens, + usageTokens, + trailingTokens, + lastUsageIndex: usageInfo.index, + }; +} + +/** + * Check if compaction should trigger based on context usage. + */ +export function shouldCompact( + contextTokens: number, + contextWindow: number, + settings: CompactionSettings, +): boolean { + if (!settings.enabled) { + return false; + } + return contextTokens > contextWindow - settings.reserveTokens; +} + +// ============================================================================ +// Cut point detection +// ============================================================================ + +/** + * Estimate token count for a message using chars/4 heuristic. + * This is conservative (overestimates tokens). + */ +export function estimateTokens(message: AgentMessage): number { + let chars = 0; + + switch (message.role) { + case "user": { + const content = (message as { content: string | Array<{ type: string; text?: string }> }) + .content; + if (typeof content === "string") { + chars = content.length; + } else if (Array.isArray(content)) { + for (const block of content) { + if (block.type === "text" && block.text) { + chars += block.text.length; + } + } + } + return Math.ceil(chars / 4); + } + case "assistant": { + const assistant = message; + for (const block of assistant.content) { + if (block.type === "text") { + chars += block.text.length; + } else if (block.type === "thinking") { + chars += block.thinking.length; + } else if (block.type === "toolCall") { + chars += block.name.length + JSON.stringify(block.arguments).length; + } + } + return Math.ceil(chars / 4); + } + case "custom": + case "toolResult": { + if (typeof message.content === "string") { + chars = message.content.length; + } else { + for (const block of message.content) { + if (block.type === "text" && block.text) { + chars += block.text.length; + } + if (block.type === "image") { + chars += 4800; // Estimate images as 4000 chars, or 1200 tokens + } + } + } + return Math.ceil(chars / 4); + } + case "bashExecution": { + chars = message.command.length + message.output.length; + return Math.ceil(chars / 4); + } + case "branchSummary": + case "compactionSummary": { + chars = message.summary.length; + return Math.ceil(chars / 4); + } + } + + return 0; +} + +/** + * Find valid cut points: indices of user, assistant, custom, or bashExecution messages. + * Never cut at tool results (they must follow their tool call). + * When we cut at an assistant message with tool calls, its tool results follow it + * and will be kept. + * BashExecutionMessage is treated like a user message (user-initiated context). + */ +function findValidCutPoints( + entries: SessionEntry[], + startIndex: number, + endIndex: number, +): number[] { + const cutPoints: number[] = []; + for (let i = startIndex; i < endIndex; i++) { + const entry = entries[i]; + switch (entry.type) { + case "message": { + const role = entry.message.role; + switch (role) { + case "bashExecution": + case "custom": + case "branchSummary": + case "compactionSummary": + case "user": + case "assistant": + cutPoints.push(i); + break; + case "toolResult": + break; + } + break; + } + case "thinking_level_change": + case "model_change": + case "compaction": + case "branch_summary": + case "custom": + case "custom_message": + case "label": + case "session_info": + break; + } + + // branch_summary and custom_message are user-role messages, valid cut points + if (entry.type === "branch_summary" || entry.type === "custom_message") { + cutPoints.push(i); + } + } + return cutPoints; +} + +/** + * Find the user message (or bashExecution) that starts the turn containing the given entry index. + * Returns -1 if no turn start found before the index. + * BashExecutionMessage is treated like a user message for turn boundaries. + */ +export function findTurnStartIndex( + entries: SessionEntry[], + entryIndex: number, + startIndex: number, +): number { + for (let i = entryIndex; i >= startIndex; i--) { + const entry = entries[i]; + // branch_summary and custom_message are user-role messages, can start a turn + if (entry.type === "branch_summary" || entry.type === "custom_message") { + return i; + } + if (entry.type === "message") { + const role = entry.message.role; + if (role === "user" || role === "bashExecution") { + return i; + } + } + } + return -1; +} + +export interface CutPointResult { + /** Index of first entry to keep */ + firstKeptEntryIndex: number; + /** Index of user message that starts the turn being split, or -1 if not splitting */ + turnStartIndex: number; + /** Whether this cut splits a turn (cut point is not a user message) */ + isSplitTurn: boolean; +} + +/** + * Find the cut point in session entries that keeps approximately `keepRecentTokens`. + * + * Algorithm: Walk backwards from newest, accumulating estimated message sizes. + * Stop when we've accumulated >= keepRecentTokens. Cut at that point. + * + * Can cut at user OR assistant messages (never tool results). When cutting at an + * assistant message with tool calls, its tool results come after and will be kept. + * + * Returns CutPointResult with: + * - firstKeptEntryIndex: the entry index to start keeping from + * - turnStartIndex: if cutting mid-turn, the user message that started that turn + * - isSplitTurn: whether we're cutting in the middle of a turn + * + * Only considers entries between `startIndex` and `endIndex` (exclusive). + */ +export function findCutPoint( + entries: SessionEntry[], + startIndex: number, + endIndex: number, + keepRecentTokens: number, +): CutPointResult { + const cutPoints = findValidCutPoints(entries, startIndex, endIndex); + + if (cutPoints.length === 0) { + return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false }; + } + + // Walk backwards from newest, accumulating estimated message sizes + let accumulatedTokens = 0; + let cutIndex = cutPoints[0]; // Default: keep from first message (not header) + + for (let i = endIndex - 1; i >= startIndex; i--) { + const entry = entries[i]; + if (entry.type !== "message") { + continue; + } + + // Estimate this message's size + const messageTokens = estimateTokens(entry.message); + accumulatedTokens += messageTokens; + + // Check if we've exceeded the budget + if (accumulatedTokens >= keepRecentTokens) { + // Find the closest valid cut point at or after this entry + for (let c = 0; c < cutPoints.length; c++) { + if (cutPoints[c] >= i) { + cutIndex = cutPoints[c]; + break; + } + } + break; + } + } + + // Scan backwards from cutIndex to include any non-message entries (bash, settings, etc.) + while (cutIndex > startIndex) { + const prevEntry = entries[cutIndex - 1]; + // Stop at session header or compaction boundaries + if (prevEntry.type === "compaction") { + break; + } + if (prevEntry.type === "message") { + // Stop if we hit any message + break; + } + // Include this non-message entry (bash, settings change, etc.) + cutIndex--; + } + + // Determine if this is a split turn + const cutEntry = entries[cutIndex]; + const isUserMessage = cutEntry.type === "message" && cutEntry.message.role === "user"; + const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex); + + return { + firstKeptEntryIndex: cutIndex, + turnStartIndex, + isSplitTurn: !isUserMessage && turnStartIndex !== -1, + }; +} + +// ============================================================================ +// Summarization +// ============================================================================ + +const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work. + +Use this EXACT format: + +## Goal +[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.] + +## Constraints & Preferences +- [Any constraints, preferences, or requirements mentioned by user] +- [Or "(none)" if none were mentioned] + +## Progress +### Done +- [x] [Completed tasks/changes] + +### In Progress +- [ ] [Current work] + +### Blocked +- [Issues preventing progress, if any] + +## Key Decisions +- **[Decision]**: [Brief rationale] + +## Next Steps +1. [Ordered list of what should happen next] + +## Critical Context +- [Any data, examples, or references needed to continue] +- [Or "(none)" if not applicable] + +Keep each section concise. Preserve exact file paths, function names, and error messages.`; + +const UPDATE_SUMMARIZATION_PROMPT = `The messages above are NEW conversation messages to incorporate into the existing summary provided in tags. + +Update the existing structured summary with new information. RULES: +- PRESERVE all existing information from the previous summary +- ADD new progress, decisions, and context from the new messages +- UPDATE the Progress section: move items from "In Progress" to "Done" when completed +- UPDATE "Next Steps" based on what was accomplished +- PRESERVE exact file paths, function names, and error messages +- If something is no longer relevant, you may remove it + +Use this EXACT format: + +## Goal +[Preserve existing goals, add new ones if the task expanded] + +## Constraints & Preferences +- [Preserve existing, add new ones discovered] + +## Progress +### Done +- [x] [Include previously done items AND newly completed items] + +### In Progress +- [ ] [Current work - update based on progress] + +### Blocked +- [Current blockers - remove if resolved] + +## Key Decisions +- **[Decision]**: [Brief rationale] (preserve all previous, add new) + +## Next Steps +1. [Update based on current state] + +## Critical Context +- [Preserve important context, add new if needed] + +Keep each section concise. Preserve exact file paths, function names, and error messages.`; + +function createSummarizationOptions( + model: Model, + maxTokens: number, + apiKey: string | undefined, + headers: Record | undefined, + signal: AbortSignal | undefined, + thinkingLevel: ThinkingLevel | undefined, +): SimpleStreamOptions { + const options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers }; + if (model.reasoning && thinkingLevel && thinkingLevel !== "off") { + options.reasoning = thinkingLevel; + } + return options; +} + +async function completeSummarization( + model: Model, + context: Context, + options: SimpleStreamOptions, + streamFn?: StreamFn, +): Promise { + if (!streamFn) { + return completeSimple(model, context, options); + } + const stream = await streamFn(model, context, options); + return stream.result(); +} + +/** + * Generate a summary of the conversation using the LLM. + * If previousSummary is provided, uses the update prompt to merge. + */ +export async function generateSummary( + currentMessages: AgentMessage[], + model: Model, + reserveTokens: number, + apiKey: string | undefined, + headers?: Record, + signal?: AbortSignal, + customInstructions?: string, + previousSummary?: string, + thinkingLevel?: ThinkingLevel, + streamFn?: StreamFn, +): Promise { + const maxTokens = Math.min( + Math.floor(0.8 * reserveTokens), + model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY, + ); + + // Use update prompt if we have a previous summary, otherwise initial prompt + let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT; + if (customInstructions) { + basePrompt = `${basePrompt}\n\nAdditional focus: ${customInstructions}`; + } + + // Serialize conversation to text so model doesn't try to continue it + // Convert to LLM messages first (handles custom types like bashExecution, custom, etc.) + const llmMessages = convertToLlm(currentMessages); + const conversationText = serializeConversation(llmMessages); + + // Build the prompt with conversation wrapped in tags + let promptText = `\n${conversationText}\n\n\n`; + if (previousSummary) { + promptText += `\n${previousSummary}\n\n\n`; + } + promptText += basePrompt; + + const summarizationMessages = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: promptText }], + timestamp: Date.now(), + }, + ]; + + const completionOptions = createSummarizationOptions( + model, + maxTokens, + apiKey, + headers, + signal, + thinkingLevel, + ); + + const response = await completeSummarization( + model, + { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, + completionOptions, + streamFn, + ); + + if (response.stopReason === "error") { + throw new Error(`Summarization failed: ${response.errorMessage || "Unknown error"}`); + } + + const textContent = response.content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join("\n"); + + return textContent; +} + +// ============================================================================ +// Compaction Preparation (for extensions) +// ============================================================================ + +export interface CompactionPreparation { + /** UUID of first entry to keep */ + firstKeptEntryId: string; + /** Messages that will be summarized and discarded */ + messagesToSummarize: AgentMessage[]; + /** Messages that will be turned into turn prefix summary (if splitting) */ + turnPrefixMessages: AgentMessage[]; + /** Whether this is a split turn (cut point in middle of turn) */ + isSplitTurn: boolean; + tokensBefore: number; + /** Summary from previous compaction, for iterative update */ + previousSummary?: string; + /** File operations extracted from messagesToSummarize */ + fileOps: FileOperations; + /** Compaction settions from settings.jsonl */ + settings: CompactionSettings; +} + +export function prepareCompaction( + pathEntries: SessionEntry[], + settings: CompactionSettings, +): CompactionPreparation | undefined { + if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") { + return undefined; + } + + let prevCompactionIndex = -1; + for (let i = pathEntries.length - 1; i >= 0; i--) { + if (pathEntries[i].type === "compaction") { + prevCompactionIndex = i; + break; + } + } + + let previousSummary: string | undefined; + let boundaryStart = 0; + if (prevCompactionIndex >= 0) { + const prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry; + previousSummary = prevCompaction.summary; + const firstKeptEntryIndex = pathEntries.findIndex( + (entry) => entry.id === prevCompaction.firstKeptEntryId, + ); + boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1; + } + const boundaryEnd = pathEntries.length; + + const tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens; + + const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens); + + // Get UUID of first kept entry + const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex]; + if (!firstKeptEntry?.id) { + return undefined; // Session needs migration + } + const firstKeptEntryId = firstKeptEntry.id; + + const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex; + + // Messages to summarize (will be discarded after summary) + const messagesToSummarize: AgentMessage[] = []; + for (let i = boundaryStart; i < historyEnd; i++) { + const msg = getMessageFromEntryForCompaction(pathEntries[i]); + if (msg) { + messagesToSummarize.push(msg); + } + } + + // Messages for turn prefix summary (if splitting a turn) + const turnPrefixMessages: AgentMessage[] = []; + if (cutPoint.isSplitTurn) { + for (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) { + const msg = getMessageFromEntryForCompaction(pathEntries[i]); + if (msg) { + turnPrefixMessages.push(msg); + } + } + } + + // Extract file operations from messages and previous compaction + const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex); + + // Also extract file ops from turn prefix if splitting + if (cutPoint.isSplitTurn) { + for (const msg of turnPrefixMessages) { + extractFileOpsFromMessage(msg, fileOps); + } + } + + return { + firstKeptEntryId, + messagesToSummarize, + turnPrefixMessages, + isSplitTurn: cutPoint.isSplitTurn, + tokensBefore, + previousSummary, + fileOps, + settings, + }; +} + +// ============================================================================ +// Main compaction function +// ============================================================================ + +const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained. + +Summarize the prefix to provide context for the retained suffix: + +## Original Request +[What did the user ask for in this turn?] + +## Early Progress +- [Key decisions and work done in the prefix] + +## Context for Suffix +- [Information needed to understand the retained recent work] + +Be concise. Focus on what's needed to understand the kept suffix.`; + +/** + * Generate summaries for compaction using prepared data. + * Returns CompactionResult - SessionManager adds uuid/parentUuid when saving. + * + * @param preparation - Pre-calculated preparation from prepareCompaction() + * @param customInstructions - Optional custom focus for the summary + */ +export async function compact( + preparation: CompactionPreparation, + model: Model, + apiKey: string | undefined, + headers?: Record, + customInstructions?: string, + signal?: AbortSignal, + thinkingLevel?: ThinkingLevel, + streamFn?: StreamFn, +): Promise { + const { + firstKeptEntryId, + messagesToSummarize, + turnPrefixMessages, + isSplitTurn, + tokensBefore, + previousSummary, + fileOps, + settings, + } = preparation; + + // Generate summaries (can be parallel if both needed) and merge into one + let summary: string; + + if (isSplitTurn && turnPrefixMessages.length > 0) { + // Generate both summaries in parallel + const [historyResult, turnPrefixResult] = await Promise.all([ + messagesToSummarize.length > 0 + ? generateSummary( + messagesToSummarize, + model, + settings.reserveTokens, + apiKey, + headers, + signal, + customInstructions, + previousSummary, + thinkingLevel, + streamFn, + ) + : Promise.resolve("No prior history."), + generateTurnPrefixSummary( + turnPrefixMessages, + model, + settings.reserveTokens, + apiKey, + headers, + signal, + thinkingLevel, + streamFn, + ), + ]); + // Merge into single summary + summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`; + } else { + // Just generate history summary + summary = await generateSummary( + messagesToSummarize, + model, + settings.reserveTokens, + apiKey, + headers, + signal, + customInstructions, + previousSummary, + thinkingLevel, + streamFn, + ); + } + + // Compute file lists and append to summary + const { readFiles, modifiedFiles } = computeFileLists(fileOps); + summary += formatFileOperations(readFiles, modifiedFiles); + + if (!firstKeptEntryId) { + throw new Error("First kept entry has no UUID - session may need migration"); + } + + return { + summary, + firstKeptEntryId, + tokensBefore, + details: { readFiles, modifiedFiles } as CompactionDetails, + }; +} + +/** + * Generate a summary for a turn prefix (when splitting a turn). + */ +async function generateTurnPrefixSummary( + messages: AgentMessage[], + model: Model, + reserveTokens: number, + apiKey: string | undefined, + headers?: Record, + signal?: AbortSignal, + thinkingLevel?: ThinkingLevel, + streamFn?: StreamFn, +): Promise { + const maxTokens = Math.min( + Math.floor(0.5 * reserveTokens), + model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY, + ); // Smaller budget for turn prefix + const llmMessages = convertToLlm(messages); + const conversationText = serializeConversation(llmMessages); + const promptText = `\n${conversationText}\n\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`; + const summarizationMessages = [ + { + role: "user" as const, + content: [{ type: "text" as const, text: promptText }], + timestamp: Date.now(), + }, + ]; + + const response = await completeSummarization( + model, + { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, + createSummarizationOptions(model, maxTokens, apiKey, headers, signal, thinkingLevel), + streamFn, + ); + + if (response.stopReason === "error") { + throw new Error( + `Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`, + ); + } + + return response.content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join("\n"); +} diff --git a/src/agents/sessions/compaction/index.ts b/src/agents/sessions/compaction/index.ts new file mode 100644 index 00000000000..d8c92a67b0b --- /dev/null +++ b/src/agents/sessions/compaction/index.ts @@ -0,0 +1,7 @@ +/** + * Compaction and summarization utilities. + */ + +export * from "./branch-summarization.js"; +export * from "./compaction.js"; +export * from "./utils.js"; diff --git a/src/agents/sessions/compaction/utils.ts b/src/agents/sessions/compaction/utils.ts new file mode 100644 index 00000000000..f82885ae382 --- /dev/null +++ b/src/agents/sessions/compaction/utils.ts @@ -0,0 +1,193 @@ +/** + * Shared utilities for compaction and branch summarization. + */ + +import type { Message } from "openclaw/plugin-sdk/llm"; +import type { AgentMessage } from "../../runtime/index.js"; + +// ============================================================================ +// File Operation Tracking +// ============================================================================ + +export interface FileOperations { + read: Set; + written: Set; + edited: Set; +} + +export function createFileOps(): FileOperations { + return { + read: new Set(), + written: new Set(), + edited: new Set(), + }; +} + +/** + * Extract file operations from tool calls in an assistant message. + */ +export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void { + if (message.role !== "assistant") { + return; + } + if (!("content" in message) || !Array.isArray(message.content)) { + return; + } + + for (const block of message.content) { + if (typeof block !== "object" || block === null) { + continue; + } + if (!("type" in block) || block.type !== "toolCall") { + continue; + } + if (!("arguments" in block) || !("name" in block)) { + continue; + } + + const args = block.arguments as Record | undefined; + if (!args) { + continue; + } + + const path = typeof args.path === "string" ? args.path : undefined; + if (!path) { + continue; + } + + switch (block.name) { + case "read": + fileOps.read.add(path); + break; + case "write": + fileOps.written.add(path); + break; + case "edit": + fileOps.edited.add(path); + break; + } + } +} + +/** + * Compute final file lists from file operations. + * Returns readFiles (files only read, not modified) and modifiedFiles. + */ +export function computeFileLists(fileOps: FileOperations): { + readFiles: string[]; + modifiedFiles: string[]; +} { + const modified = new Set([...fileOps.edited, ...fileOps.written]); + const readOnly = [...fileOps.read].filter((f) => !modified.has(f)).toSorted(); + const modifiedFiles = [...modified].toSorted(); + return { readFiles: readOnly, modifiedFiles }; +} + +/** + * Format file operations as XML tags for summary. + */ +export function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string { + const sections: string[] = []; + if (readFiles.length > 0) { + sections.push(`\n${readFiles.join("\n")}\n`); + } + if (modifiedFiles.length > 0) { + sections.push(`\n${modifiedFiles.join("\n")}\n`); + } + if (sections.length === 0) { + return ""; + } + return `\n\n${sections.join("\n\n")}`; +} + +// ============================================================================ +// Message Serialization +// ============================================================================ + +/** Maximum characters for a tool result in serialized summaries. */ +const TOOL_RESULT_MAX_CHARS = 2000; + +/** + * Truncate text to a maximum character length for summarization. + * Keeps the beginning and appends a truncation marker. + */ +function truncateForSummary(text: string, maxChars: number): string { + if (text.length <= maxChars) { + return text; + } + const truncatedChars = text.length - maxChars; + return `${text.slice(0, maxChars)}\n\n[... ${truncatedChars} more characters truncated]`; +} + +/** + * Serialize LLM messages to text for summarization. + * This prevents the model from treating it as a conversation to continue. + * Call convertToLlm() first to handle custom message types. + * + * Tool results are truncated to keep the summarization request within + * reasonable token budgets. Full content is not needed for summarization. + */ +export function serializeConversation(messages: Message[]): string { + const parts: string[] = []; + + for (const msg of messages) { + if (msg.role === "user") { + const content = + typeof msg.content === "string" + ? msg.content + : msg.content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join(""); + if (content) { + parts.push(`[User]: ${content}`); + } + } else if (msg.role === "assistant") { + const textParts: string[] = []; + const thinkingParts: string[] = []; + const toolCalls: string[] = []; + + for (const block of msg.content) { + if (block.type === "text") { + textParts.push(block.text); + } else if (block.type === "thinking") { + thinkingParts.push(block.thinking); + } else if (block.type === "toolCall") { + const args = block.arguments; + const argsStr = Object.entries(args) + .map(([k, v]) => `${k}=${JSON.stringify(v)}`) + .join(", "); + toolCalls.push(`${block.name}(${argsStr})`); + } + } + + if (thinkingParts.length > 0) { + parts.push(`[Assistant thinking]: ${thinkingParts.join("\n")}`); + } + if (textParts.length > 0) { + parts.push(`[Assistant]: ${textParts.join("\n")}`); + } + if (toolCalls.length > 0) { + parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`); + } + } else if (msg.role === "toolResult") { + const content = msg.content + .filter((c): c is { type: "text"; text: string } => c.type === "text") + .map((c) => c.text) + .join(""); + if (content) { + parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`); + } + } + } + + return parts.join("\n\n"); +} + +// ============================================================================ +// Summarization System Prompt +// ============================================================================ + +export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified. + +Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`; diff --git a/src/agents/sessions/defaults.ts b/src/agents/sessions/defaults.ts new file mode 100644 index 00000000000..fca09bd592b --- /dev/null +++ b/src/agents/sessions/defaults.ts @@ -0,0 +1,3 @@ +import type { ThinkingLevel } from "../runtime/index.js"; + +export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "medium"; diff --git a/src/agents/sessions/diagnostics.ts b/src/agents/sessions/diagnostics.ts new file mode 100644 index 00000000000..693a4ac8d88 --- /dev/null +++ b/src/agents/sessions/diagnostics.ts @@ -0,0 +1,15 @@ +export interface ResourceCollision { + resourceType: "extension" | "skill" | "prompt" | "theme"; + name: string; // skill name, command/tool/flag name, prompt name, theme name + winnerPath: string; + loserPath: string; + winnerSource?: string; // e.g., "npm:foo", "git:...", "local" + loserSource?: string; +} + +export interface ResourceDiagnostic { + type: "warning" | "error" | "collision"; + message: string; + path?: string; + collision?: ResourceCollision; +} diff --git a/src/agents/sessions/event-bus.ts b/src/agents/sessions/event-bus.ts new file mode 100644 index 00000000000..870e393f038 --- /dev/null +++ b/src/agents/sessions/event-bus.ts @@ -0,0 +1,33 @@ +import { EventEmitter } from "node:events"; + +export interface EventBus { + emit(channel: string, data: unknown): void; + on(channel: string, handler: (data: unknown) => void): () => void; +} + +export interface EventBusController extends EventBus { + clear(): void; +} + +export function createEventBus(): EventBusController { + const emitter = new EventEmitter(); + return { + emit: (channel, data) => { + emitter.emit(channel, data); + }, + on: (channel, handler) => { + const safeHandler = (data: unknown) => { + try { + handler(data); + } catch (err) { + console.error(`Event handler error (${channel}):`, err); + } + }; + emitter.on(channel, safeHandler); + return () => emitter.off(channel, safeHandler); + }, + clear: () => { + emitter.removeAllListeners(); + }, + }; +} diff --git a/src/agents/sessions/exec.ts b/src/agents/sessions/exec.ts new file mode 100644 index 00000000000..223f85a65be --- /dev/null +++ b/src/agents/sessions/exec.ts @@ -0,0 +1,111 @@ +/** + * Shared command execution utilities for extensions and custom tools. + */ + +import { spawn } from "node:child_process"; +import { waitForChildProcess } from "../utils/child-process.js"; + +/** + * Options for executing shell commands. + */ +export interface ExecOptions { + /** AbortSignal to cancel the command */ + signal?: AbortSignal; + /** Timeout in milliseconds */ + timeout?: number; + /** Working directory */ + cwd?: string; +} + +/** + * Result of executing a shell command. + */ +export interface ExecResult { + stdout: string; + stderr: string; + code: number; + killed: boolean; +} + +/** + * Execute a shell command and return stdout/stderr/code. + * Supports timeout and abort signal. + */ +export async function execCommand( + command: string, + args: string[], + cwd: string, + options?: ExecOptions, +): Promise { + return new Promise((resolve) => { + const proc = spawn(command, args, { + cwd, + shell: false, + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdout = ""; + let stderr = ""; + let killed = false; + let timeoutId: NodeJS.Timeout | undefined; + + const killProcess = () => { + if (!killed) { + killed = true; + proc.kill("SIGTERM"); + // Force kill after 5 seconds if SIGTERM doesn't work + setTimeout(() => { + if (!proc.killed) { + proc.kill("SIGKILL"); + } + }, 5000); + } + }; + + // Handle abort signal + if (options?.signal) { + if (options.signal.aborted) { + killProcess(); + } else { + options.signal.addEventListener("abort", killProcess, { once: true }); + } + } + + // Handle timeout + if (options?.timeout && options.timeout > 0) { + timeoutId = setTimeout(() => { + killProcess(); + }, options.timeout); + } + + proc.stdout?.on("data", (data) => { + stdout += data.toString(); + }); + + proc.stderr?.on("data", (data) => { + stderr += data.toString(); + }); + + // Wait for process termination without hanging on inherited stdio handles + // held open by detached descendants. + waitForChildProcess(proc) + .then((code) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (options?.signal) { + options.signal.removeEventListener("abort", killProcess); + } + resolve({ stdout, stderr, code: code ?? 0, killed }); + }) + .catch(() => { + if (timeoutId) { + clearTimeout(timeoutId); + } + if (options?.signal) { + options.signal.removeEventListener("abort", killProcess); + } + resolve({ stdout, stderr, code: 1, killed }); + }); + }); +} diff --git a/src/agents/sessions/extension-sdk.ts b/src/agents/sessions/extension-sdk.ts new file mode 100644 index 00000000000..87c0cef10fb --- /dev/null +++ b/src/agents/sessions/extension-sdk.ts @@ -0,0 +1,56 @@ +/** + * Extension-safe session SDK surface. + * + * Keep this barrel free of the session runtime and resource loader. The + * extension loader imports it to virtualize `openclaw/plugin-sdk/agent-sessions`, + * so importing loader-owned modules here creates runtime cycles. + */ + +export { getAgentDir, VERSION } from "../config.js"; +export * from "./auth-storage.js"; +export * from "./bash-executor.js"; +export * from "./compaction/index.js"; +export * from "./event-bus.js"; +export type { ReadonlyFooterDataProvider } from "./footer-data-provider.js"; +export { convertToLlm } from "./messages.js"; +export * from "./model-registry.js"; +export * from "./model-resolver.js"; +export * from "./package-manager.js"; +export type { PromptTemplate } from "./prompt-templates.js"; +export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.js"; +export * from "./session-manager.js"; +export { + FileSettingsStorage, + InMemorySettingsStorage, + SettingsManager, + type BranchSummarySettings, + type ImageSettings, + type MarkdownSettings, + type PackageSource, + type ProviderRetrySettings, + type RetrySettings, + type Settings, + type SettingsError, + type SettingsScope, + type SettingsStorage, + type TerminalSettings, + type ThinkingBudgetsSettings, + type TransportSetting, + type WarningSettings, +} from "./settings-manager.js"; +export type { Skill } from "./skills.js"; +export * from "./source-info.js"; +export * from "./tools/index.js"; +export type * from "./extensions/types.js"; +export { + defineTool, + isBashToolResult, + isEditToolResult, + isFindToolResult, + isGrepToolResult, + isLsToolResult, + isReadToolResult, + isToolCallEventType, + isWriteToolResult, +} from "./extensions/types.js"; +export { wrapRegisteredTool, wrapRegisteredTools } from "./extensions/wrapper.js"; diff --git a/src/agents/sessions/extensions/index.ts b/src/agents/sessions/extensions/index.ts new file mode 100644 index 00000000000..7439f208749 --- /dev/null +++ b/src/agents/sessions/extensions/index.ts @@ -0,0 +1,172 @@ +/** + * Extension system for lifecycle events and custom tools. + */ + +export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.js"; +export type { SourceInfo } from "../source-info.js"; +export { + createExtensionRuntime, + discoverAndLoadExtensions, + loadExtensionFromFactory, + loadExtensions, +} from "./loader.js"; +export type { + ExtensionErrorListener, + ForkHandler, + NavigateTreeHandler, + NewSessionHandler, + ShutdownHandler, + SwitchSessionHandler, +} from "./runner.js"; +export { ExtensionRunner } from "./runner.js"; +export type { + AfterProviderResponseEvent, + AgentEndEvent, + AgentStartEvent, + // Re-exports + AgentToolResult, + AgentToolUpdateCallback, + AppendEntryHandler, + // App keybindings (for custom editors) + AppKeybinding, + AutocompleteProviderFactory, + // Events - Tool (ToolCallEvent types) + BashToolCallEvent, + BashToolResultEvent, + BeforeAgentStartEvent, + BeforeAgentStartEventResult, + BeforeProviderRequestEvent, + BeforeProviderRequestEventResult, + BuildSystemPromptOptions, + // Context + CompactOptions, + // Events - Agent + ContextEvent, + // Event Results + ContextEventResult, + ContextUsage, + CustomToolCallEvent, + CustomToolResultEvent, + EditorFactory, + EditToolCallEvent, + EditToolResultEvent, + ExecOptions, + ExecResult, + Extension, + ExtensionActions, + // API + ExtensionAPI, + ExtensionCommandContext, + ExtensionCommandContextActions, + ExtensionContext, + ExtensionContextActions, + // Errors + ExtensionError, + ExtensionEvent, + ExtensionFactory, + ExtensionFlag, + ExtensionHandler, + // Runtime + ExtensionRuntime, + ExtensionShortcut, + ExtensionUIContext, + ExtensionUIDialogOptions, + ExtensionWidgetOptions, + FindToolCallEvent, + FindToolResultEvent, + GetActiveToolsHandler, + GetAllToolsHandler, + GetCommandsHandler, + GetThinkingLevelHandler, + GrepToolCallEvent, + GrepToolResultEvent, + // Events - Input + InputEvent, + InputEventResult, + InputSource, + KeybindingsManager, + LoadExtensionsResult, + LsToolCallEvent, + LsToolResultEvent, + // Events - Message + MessageEndEvent, + // Message Rendering + MessageRenderer, + MessageRenderOptions, + MessageStartEvent, + MessageUpdateEvent, + ModelSelectEvent, + ModelSelectSource, + // Provider Registration + ProviderConfig, + ProviderModelConfig, + ReadToolCallEvent, + ReadToolResultEvent, + // Commands + RegisteredCommand, + RegisteredTool, + ReplacedSessionContext, + ResolvedCommand, + // Events - Resources + ResourcesDiscoverEvent, + ResourcesDiscoverResult, + SendMessageHandler, + SendUserMessageHandler, + SessionBeforeCompactEvent, + SessionBeforeCompactResult, + SessionBeforeForkEvent, + SessionBeforeForkResult, + SessionBeforeSwitchEvent, + SessionBeforeSwitchResult, + SessionBeforeTreeEvent, + SessionBeforeTreeResult, + SessionCompactEvent, + SessionEvent, + SessionShutdownEvent, + // Events - Session + SessionStartEvent, + SessionTreeEvent, + SetActiveToolsHandler, + SetLabelHandler, + SetModelHandler, + SetThinkingLevelHandler, + TerminalInputHandler, + // Events - Tool + ToolCallEvent, + ToolCallEventResult, + // Tools + ToolDefinition, + // Events - Tool Execution + ToolExecutionEndEvent, + // Tool execution mode + ToolExecutionMode, + ToolExecutionStartEvent, + ToolExecutionUpdateEvent, + ToolInfo, + ToolRenderResultOptions, + ToolResultEvent, + ToolResultEventResult, + TreePreparation, + TurnEndEvent, + TurnStartEvent, + // Events - User Bash + UserBashEvent, + UserBashEventResult, + WidgetPlacement, + WorkingIndicatorOptions, + WriteToolCallEvent, + WriteToolResultEvent, +} from "./types.js"; +// Type guards +export { + defineTool, + isBashToolResult, + isEditToolResult, + isFindToolResult, + isGrepToolResult, + isLsToolResult, + isReadToolResult, + isToolCallEventType, + isWriteToolResult, +} from "./types.js"; +export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.js"; diff --git a/src/agents/sessions/extensions/loader.test.ts b/src/agents/sessions/extensions/loader.test.ts new file mode 100644 index 00000000000..d428ac26f34 --- /dev/null +++ b/src/agents/sessions/extensions/loader.test.ts @@ -0,0 +1,53 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { loadExtensions } from "./loader.js"; + +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { force: true, recursive: true }))); +}); + +describe("loadExtensions", () => { + it("resolves public LLM plugin SDK subpaths in jiti-loaded extensions", async () => { + const dir = await mkdtemp(join(tmpdir(), "openclaw-extension-sdk-")); + tempDirs.push(dir); + const extensionPath = join(dir, "extension.ts"); + await writeFile( + extensionPath, + ` +import * as llmAnthropic from "openclaw/plugin-sdk/llm-anthropic"; +import * as llmBedrock from "openclaw/plugin-sdk/llm-bedrock"; +import * as llmGoogleShared from "openclaw/plugin-sdk/llm-google-shared"; +import * as llmOpenAiCodexResponses from "openclaw/plugin-sdk/llm-openai-codex-responses"; +import * as llmOpenAiCompletions from "openclaw/plugin-sdk/llm-openai-completions"; +import * as llmOpenAiResponses from "openclaw/plugin-sdk/llm-openai-responses"; +import * as llmProviderRuntime from "openclaw/plugin-sdk/llm-provider-runtime"; + +export default async function(api) { + if (!llmBedrock.supportsBedrockPromptCaching("anthropic.claude-3-7-sonnet")) { + throw new Error("bedrock helper unavailable"); + } + void llmAnthropic; + void llmGoogleShared; + void llmOpenAiCodexResponses; + void llmOpenAiCompletions; + void llmOpenAiResponses; + void llmProviderRuntime; + api.registerCommand("sdk-subpath-probe", { + description: "probe", + handler() {}, + }); +} +`, + ); + + const result = await loadExtensions([extensionPath], dir); + + expect(result.errors).toEqual([]); + expect(result.extensions).toHaveLength(1); + expect(result.extensions[0]?.commands.has("sdk-subpath-probe")).toBe(true); + }); +}); diff --git a/src/agents/sessions/extensions/loader.ts b/src/agents/sessions/extensions/loader.ts new file mode 100644 index 00000000000..8b6b49c9d1c --- /dev/null +++ b/src/agents/sessions/extensions/loader.ts @@ -0,0 +1,654 @@ +/** + * Extension loader - loads TypeScript extension modules using jiti. + * + */ + +import * as fs from "node:fs"; +import { createRequire } from "node:module"; +import * as os from "node:os"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; +import type { KeyId } from "@earendil-works/pi-tui"; +import * as bundledTui from "@earendil-works/pi-tui"; +import { createJiti } from "jiti/static"; +import * as bundledLlm from "openclaw/plugin-sdk/llm"; +import * as bundledLlmAnthropic from "openclaw/plugin-sdk/llm-anthropic"; +import * as bundledLlmBedrock from "openclaw/plugin-sdk/llm-bedrock"; +import * as bundledLlmGoogleShared from "openclaw/plugin-sdk/llm-google-shared"; +import * as bundledLlmOauth from "openclaw/plugin-sdk/llm-oauth"; +import * as bundledLlmOpenAiCodexResponses from "openclaw/plugin-sdk/llm-openai-codex-responses"; +import * as bundledLlmOpenAiCompletions from "openclaw/plugin-sdk/llm-openai-completions"; +import * as bundledLlmOpenAiResponses from "openclaw/plugin-sdk/llm-openai-responses"; +import * as bundledLlmProviderRuntime from "openclaw/plugin-sdk/llm-provider-runtime"; +// Static imports of packages that extensions may use. +// These MUST be static so Bun bundles them into the compiled binary. +// The virtualModules option then makes them available to extensions. +import * as bundledTypebox from "typebox"; +import * as bundledTypeboxCompile from "typebox/compile"; +import * as bundledTypeboxValue from "typebox/value"; +import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js"; +import * as bundledAgentCore from "../../runtime/index.js"; +import { createEventBus, type EventBus } from "../event-bus.js"; +import type { ExecOptions } from "../exec.js"; +import { execCommand } from "../exec.js"; +import * as bundledAgentSessions from "../extension-sdk.js"; +import { createSyntheticSourceInfo } from "../source-info.js"; +import type { + Extension, + ExtensionAPI, + ExtensionFactory, + ExtensionRuntime, + LoadExtensionsResult, + MessageRenderer, + ProviderConfig, + RegisteredCommand, + ToolDefinition, +} from "./types.js"; + +/** Modules available to extensions via virtualModules (for compiled Bun binary) */ +const VIRTUAL_MODULES: Record = { + typebox: bundledTypebox, + "typebox/compile": bundledTypeboxCompile, + "typebox/value": bundledTypeboxValue, + "@sinclair/typebox": bundledTypebox, + "@sinclair/typebox/compile": bundledTypeboxCompile, + "@sinclair/typebox/value": bundledTypeboxValue, + "openclaw/plugin-sdk/agent-core": bundledAgentCore, + "@earendil-works/pi-tui": bundledTui, + "openclaw/plugin-sdk/llm": bundledLlm, + "openclaw/plugin-sdk/llm-anthropic": bundledLlmAnthropic, + "openclaw/plugin-sdk/llm-bedrock": bundledLlmBedrock, + "openclaw/plugin-sdk/llm-google-shared": bundledLlmGoogleShared, + "openclaw/plugin-sdk/llm-oauth": bundledLlmOauth, + "openclaw/plugin-sdk/llm-openai-codex-responses": bundledLlmOpenAiCodexResponses, + "openclaw/plugin-sdk/llm-openai-completions": bundledLlmOpenAiCompletions, + "openclaw/plugin-sdk/llm-openai-responses": bundledLlmOpenAiResponses, + "openclaw/plugin-sdk/llm-provider-runtime": bundledLlmProviderRuntime, + "openclaw/plugin-sdk/agent-sessions": bundledAgentSessions, +}; + +const require = createRequire(import.meta.url); + +/** + * Get aliases for jiti (used in Node.js/development mode). + * In Bun binary mode, virtualModules is used instead. + */ +let aliases: Record | null = null; + +function getAliases(): Record { + if (aliases) { + return aliases; + } + + const currentDirname = path.dirname(fileURLToPath(import.meta.url)); + const agentSessionsEntry = path.resolve(currentDirname, "..", "extension-sdk.js"); + + const typeboxEntry = require.resolve("typebox"); + const typeboxCompileEntry = require.resolve("typebox/compile"); + const typeboxValueEntry = require.resolve("typebox/value"); + + const agentCoreEntry = fileURLToPath(import.meta.resolve("openclaw/plugin-sdk/agent-core")); + const tuiEntry = fileURLToPath(import.meta.resolve("@earendil-works/pi-tui")); + const llmEntry = fileURLToPath(import.meta.resolve("openclaw/plugin-sdk/llm")); + const llmAnthropicEntry = fileURLToPath(import.meta.resolve("openclaw/plugin-sdk/llm-anthropic")); + const llmBedrockEntry = fileURLToPath(import.meta.resolve("openclaw/plugin-sdk/llm-bedrock")); + const llmGoogleSharedEntry = fileURLToPath( + import.meta.resolve("openclaw/plugin-sdk/llm-google-shared"), + ); + const llmOauthEntry = fileURLToPath(import.meta.resolve("openclaw/plugin-sdk/llm-oauth")); + const llmOpenAiCodexResponsesEntry = fileURLToPath( + import.meta.resolve("openclaw/plugin-sdk/llm-openai-codex-responses"), + ); + const llmOpenAiCompletionsEntry = fileURLToPath( + import.meta.resolve("openclaw/plugin-sdk/llm-openai-completions"), + ); + const llmOpenAiResponsesEntry = fileURLToPath( + import.meta.resolve("openclaw/plugin-sdk/llm-openai-responses"), + ); + const llmProviderRuntimeEntry = fileURLToPath( + import.meta.resolve("openclaw/plugin-sdk/llm-provider-runtime"), + ); + + aliases = { + "openclaw/plugin-sdk/agent-sessions": agentSessionsEntry, + "openclaw/plugin-sdk/agent-core": agentCoreEntry, + "@earendil-works/pi-tui": tuiEntry, + "openclaw/plugin-sdk/llm": llmEntry, + "openclaw/plugin-sdk/llm-anthropic": llmAnthropicEntry, + "openclaw/plugin-sdk/llm-bedrock": llmBedrockEntry, + "openclaw/plugin-sdk/llm-google-shared": llmGoogleSharedEntry, + "openclaw/plugin-sdk/llm-oauth": llmOauthEntry, + "openclaw/plugin-sdk/llm-openai-codex-responses": llmOpenAiCodexResponsesEntry, + "openclaw/plugin-sdk/llm-openai-completions": llmOpenAiCompletionsEntry, + "openclaw/plugin-sdk/llm-openai-responses": llmOpenAiResponsesEntry, + "openclaw/plugin-sdk/llm-provider-runtime": llmProviderRuntimeEntry, + typebox: typeboxEntry, + "typebox/compile": typeboxCompileEntry, + "typebox/value": typeboxValueEntry, + "@sinclair/typebox": typeboxEntry, + "@sinclair/typebox/compile": typeboxCompileEntry, + "@sinclair/typebox/value": typeboxValueEntry, + }; + + return aliases; +} + +const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; + +function normalizeUnicodeSpaces(str: string): string { + return str.replace(UNICODE_SPACES, " "); +} + +function expandPath(p: string): string { + const normalized = normalizeUnicodeSpaces(p); + if (normalized.startsWith("~/")) { + return path.join(os.homedir(), normalized.slice(2)); + } + if (normalized.startsWith("~")) { + return path.join(os.homedir(), normalized.slice(1)); + } + return normalized; +} + +function resolvePath(extPath: string, cwd: string): string { + const expanded = expandPath(extPath); + if (path.isAbsolute(expanded)) { + return expanded; + } + return path.resolve(cwd, expanded); +} + +type HandlerFn = (...args: unknown[]) => Promise; + +/** + * Create a runtime with throwing stubs for action methods. + * Runner.bindCore() replaces these with real implementations. + */ +export function createExtensionRuntime(): ExtensionRuntime { + const notInitialized = () => { + throw new Error( + "Extension runtime not initialized. Action methods cannot be called during extension loading.", + ); + }; + const state: { staleMessage?: string } = {}; + const assertActive = () => { + if (state.staleMessage) { + throw new Error(state.staleMessage); + } + }; + + const runtime: ExtensionRuntime = { + sendMessage: notInitialized, + sendUserMessage: notInitialized, + appendEntry: notInitialized, + setSessionName: notInitialized, + getSessionName: notInitialized, + setLabel: notInitialized, + getActiveTools: notInitialized, + getAllTools: notInitialized, + setActiveTools: notInitialized, + // registerTool() is valid during extension load; refresh is only needed post-bind. + refreshTools: () => {}, + getCommands: notInitialized, + setModel: () => Promise.reject(new Error("Extension runtime not initialized")), + getThinkingLevel: notInitialized, + setThinkingLevel: notInitialized, + flagValues: new Map(), + pendingProviderRegistrations: [], + assertActive, + invalidate: (message) => { + state.staleMessage ??= + message ?? + "This extension ctx is stale after session replacement or reload. Do not use a captured api or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload()."; + }, + // Pre-bind: queue registrations so bindCore() can flush them once the + // model registry is available. bindCore() replaces both with direct calls. + registerProvider: (name, config, extensionPath = "") => { + runtime.pendingProviderRegistrations.push({ name, config, extensionPath }); + }, + unregisterProvider: (name) => { + runtime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter( + (r) => r.name !== name, + ); + }, + }; + + return runtime; +} + +/** + * Create the ExtensionAPI for an extension. + * Registration methods write to the extension object. + * Action methods delegate to the shared runtime. + */ +function createExtensionAPI( + extension: Extension, + runtime: ExtensionRuntime, + cwd: string, + eventBus: EventBus, +): ExtensionAPI { + const api = { + // Registration methods - write to extension + on(event: string, handler: HandlerFn): void { + runtime.assertActive(); + const list = extension.handlers.get(event) ?? []; + list.push(handler); + extension.handlers.set(event, list); + }, + + registerTool(tool: ToolDefinition): void { + runtime.assertActive(); + extension.tools.set(tool.name, { + definition: tool, + sourceInfo: extension.sourceInfo, + }); + runtime.refreshTools(); + }, + + registerCommand(name: string, options: Omit): void { + runtime.assertActive(); + extension.commands.set(name, { + name, + sourceInfo: extension.sourceInfo, + ...options, + }); + }, + + registerShortcut( + shortcut: KeyId, + options: { + description?: string; + handler: (ctx: import("./types.js").ExtensionContext) => Promise | void; + }, + ): void { + runtime.assertActive(); + extension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options }); + }, + + registerFlag( + name: string, + options: { description?: string; type: "boolean" | "string"; default?: boolean | string }, + ): void { + runtime.assertActive(); + extension.flags.set(name, { name, extensionPath: extension.path, ...options }); + if (options.default !== undefined && !runtime.flagValues.has(name)) { + runtime.flagValues.set(name, options.default); + } + }, + + registerMessageRenderer(customType: string, renderer: MessageRenderer): void { + runtime.assertActive(); + extension.messageRenderers.set(customType, renderer as MessageRenderer); + }, + + // Flag access - checks extension registered it, reads from runtime + getFlag(name: string): boolean | string | undefined { + runtime.assertActive(); + if (!extension.flags.has(name)) { + return undefined; + } + return runtime.flagValues.get(name); + }, + + // Action methods - delegate to shared runtime + sendMessage(message, options): void { + runtime.assertActive(); + runtime.sendMessage(message, options); + }, + + sendUserMessage(content, options): void { + runtime.assertActive(); + runtime.sendUserMessage(content, options); + }, + + appendEntry(customType: string, data?: unknown): void { + runtime.assertActive(); + runtime.appendEntry(customType, data); + }, + + setSessionName(name: string): void { + runtime.assertActive(); + runtime.setSessionName(name); + }, + + getSessionName(): string | undefined { + runtime.assertActive(); + return runtime.getSessionName(); + }, + + setLabel(entryId: string, label: string | undefined): void { + runtime.assertActive(); + runtime.setLabel(entryId, label); + }, + + exec(command: string, args: string[], options?: ExecOptions) { + runtime.assertActive(); + return execCommand(command, args, options?.cwd ?? cwd, options); + }, + + getActiveTools(): string[] { + runtime.assertActive(); + return runtime.getActiveTools(); + }, + + getAllTools() { + runtime.assertActive(); + return runtime.getAllTools(); + }, + + setActiveTools(toolNames: string[]): void { + runtime.assertActive(); + runtime.setActiveTools(toolNames); + }, + + getCommands() { + runtime.assertActive(); + return runtime.getCommands(); + }, + + setModel(model) { + runtime.assertActive(); + return runtime.setModel(model); + }, + + getThinkingLevel() { + runtime.assertActive(); + return runtime.getThinkingLevel(); + }, + + setThinkingLevel(level) { + runtime.assertActive(); + runtime.setThinkingLevel(level); + }, + + registerProvider(name: string, config: ProviderConfig) { + runtime.assertActive(); + runtime.registerProvider(name, config, extension.path); + }, + + unregisterProvider(name: string) { + runtime.assertActive(); + runtime.unregisterProvider(name, extension.path); + }, + + events: eventBus, + } as ExtensionAPI; + + return api; +} + +async function loadExtensionModule(extensionPath: string) { + const jiti = createJiti(import.meta.url, { + moduleCache: false, + // In Bun binary: use virtualModules for bundled packages (no filesystem resolution) + // Also disable tryNative so jiti handles ALL imports (not just the entry point) + // In Node.js/dev: use aliases to resolve to node_modules paths + ...(isBunBinary + ? { virtualModules: VIRTUAL_MODULES, tryNative: false } + : { alias: getAliases() }), + }); + + const module = await jiti.import(extensionPath, { default: true }); + const factory = module as ExtensionFactory; + return typeof factory !== "function" ? undefined : factory; +} + +/** + * Create an Extension object with empty collections. + */ +function createExtension(extensionPath: string, resolvedPath: string): Extension { + const source = + extensionPath.startsWith("<") && extensionPath.endsWith(">") + ? extensionPath.slice(1, -1).split(":")[0] || "temporary" + : "local"; + const baseDir = extensionPath.startsWith("<") ? undefined : path.dirname(resolvedPath); + + return { + path: extensionPath, + resolvedPath, + sourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }), + handlers: new Map(), + tools: new Map(), + messageRenderers: new Map(), + commands: new Map(), + flags: new Map(), + shortcuts: new Map(), + }; +} + +async function loadExtension( + extensionPath: string, + cwd: string, + eventBus: EventBus, + runtime: ExtensionRuntime, +): Promise<{ extension: Extension | null; error: string | null }> { + const resolvedPath = resolvePath(extensionPath, cwd); + + try { + const factory = await loadExtensionModule(resolvedPath); + if (!factory) { + return { + extension: null, + error: `Extension does not export a valid factory function: ${extensionPath}`, + }; + } + + const extension = createExtension(extensionPath, resolvedPath); + const api = createExtensionAPI(extension, runtime, cwd, eventBus); + await factory(api); + + return { extension, error: null }; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + return { extension: null, error: `Failed to load extension: ${message}` }; + } +} + +/** + * Create an Extension from an inline factory function. + */ +export async function loadExtensionFromFactory( + factory: ExtensionFactory, + cwd: string, + eventBus: EventBus, + runtime: ExtensionRuntime, + extensionPath = "", +): Promise { + const extension = createExtension(extensionPath, extensionPath); + const api = createExtensionAPI(extension, runtime, cwd, eventBus); + await factory(api); + return extension; +} + +/** + * Load extensions from paths. + */ +export async function loadExtensions( + paths: string[], + cwd: string, + eventBus?: EventBus, +): Promise { + const extensions: Extension[] = []; + const errors: Array<{ path: string; error: string }> = []; + const resolvedEventBus = eventBus ?? createEventBus(); + const runtime = createExtensionRuntime(); + + for (const extPath of paths) { + const { extension, error } = await loadExtension(extPath, cwd, resolvedEventBus, runtime); + + if (error) { + errors.push({ path: extPath, error }); + continue; + } + + if (extension) { + extensions.push(extension); + } + } + + return { + extensions, + errors, + runtime, + }; +} + +interface ResourceManifest { + extensions?: string[]; + themes?: string[]; + skills?: string[]; + prompts?: string[]; +} + +function readResourceManifest(packageJsonPath: string): ResourceManifest | null { + try { + const content = fs.readFileSync(packageJsonPath, "utf-8"); + const pkg = JSON.parse(content); + if (pkg.openclaw && typeof pkg.openclaw === "object") { + return pkg.openclaw as ResourceManifest; + } + return null; + } catch { + return null; + } +} + +function isExtensionFile(name: string): boolean { + return name.endsWith(".ts") || name.endsWith(".js"); +} + +/** + * Resolve extension entry points from a directory. + * + * Checks for: + * 1. package.json with "openclaw.extensions" field -> returns declared paths + * 2. index.ts or index.js -> returns the index file + * + * Returns resolved paths or null if no entry points found. + */ +function resolveExtensionEntries(dir: string): string[] | null { + // Check for package.json with "openclaw" field first + const packageJsonPath = path.join(dir, "package.json"); + if (fs.existsSync(packageJsonPath)) { + const manifest = readResourceManifest(packageJsonPath); + if (manifest?.extensions?.length) { + const entries: string[] = []; + for (const extPath of manifest.extensions) { + const resolvedExtPath = path.resolve(dir, extPath); + if (fs.existsSync(resolvedExtPath)) { + entries.push(resolvedExtPath); + } + } + if (entries.length > 0) { + return entries; + } + } + } + + // Check for index.ts or index.js + const indexTs = path.join(dir, "index.ts"); + const indexJs = path.join(dir, "index.js"); + if (fs.existsSync(indexTs)) { + return [indexTs]; + } + if (fs.existsSync(indexJs)) { + return [indexJs]; + } + + return null; +} + +/** + * Discover extensions in a directory. + * + * Discovery rules: + * 1. Direct files: `extensions/*.ts` or `*.js` → load + * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load + * 3. Subdirectory with package.json: `extensions/* /package.json` with "openclaw" field → load what it declares + * + * No recursion beyond one level. Complex packages must use package.json manifest. + */ +function discoverExtensionsInDir(dir: string): string[] { + if (!fs.existsSync(dir)) { + return []; + } + + const discovered: string[] = []; + + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const entryPath = path.join(dir, entry.name); + + // 1. Direct files: *.ts or *.js + if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) { + discovered.push(entryPath); + continue; + } + + // 2 & 3. Subdirectories + if (entry.isDirectory() || entry.isSymbolicLink()) { + const entries = resolveExtensionEntries(entryPath); + if (entries) { + discovered.push(...entries); + } + } + } + } catch { + return []; + } + + return discovered; +} + +/** + * Discover and load extensions from standard locations. + */ +export async function discoverAndLoadExtensions( + configuredPaths: string[], + cwd: string, + agentDir: string = getAgentDir(), + eventBus?: EventBus, +): Promise { + const allPaths: string[] = []; + const seen = new Set(); + + const addPaths = (paths: string[]) => { + for (const p of paths) { + const resolved = path.resolve(p); + if (!seen.has(resolved)) { + seen.add(resolved); + allPaths.push(p); + } + } + }; + + // 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/ + const localExtDir = path.join(cwd, CONFIG_DIR_NAME, "extensions"); + addPaths(discoverExtensionsInDir(localExtDir)); + + // 2. Global extensions: agentDir/extensions/ + const globalExtDir = path.join(agentDir, "extensions"); + addPaths(discoverExtensionsInDir(globalExtDir)); + + // 3. Explicitly configured paths + for (const p of configuredPaths) { + const resolved = resolvePath(p, cwd); + if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) { + // Check for package.json with OpenClaw manifest or index.ts + const entries = resolveExtensionEntries(resolved); + if (entries) { + addPaths(entries); + continue; + } + // No explicit entries - discover individual files in directory + addPaths(discoverExtensionsInDir(resolved)); + continue; + } + + addPaths([resolved]); + } + + return loadExtensions(allPaths, cwd, eventBus); +} diff --git a/src/agents/sessions/extensions/runner.ts b/src/agents/sessions/extensions/runner.ts new file mode 100644 index 00000000000..8c4b2ecf8cb --- /dev/null +++ b/src/agents/sessions/extensions/runner.ts @@ -0,0 +1,1147 @@ +/** + * Extension runner - executes extensions and manages their lifecycle. + */ + +import type { KeyId } from "@earendil-works/pi-tui"; +import type { ImageContent, Model } from "openclaw/plugin-sdk/llm"; +import { type Theme, theme } from "../../modes/interactive/theme/theme.js"; +import type { AgentMessage } from "../../runtime/index.js"; +import type { ResourceDiagnostic } from "../diagnostics.js"; +import type { KeybindingsConfig } from "../keybindings.js"; +import type { ModelRegistry } from "../model-registry.js"; +import type { SessionManager } from "../session-manager.js"; +import type { BuildSystemPromptOptions } from "../system-prompt.js"; +import type { + BeforeAgentStartEvent, + BeforeAgentStartEventResult, + BeforeProviderRequestEvent, + CompactOptions, + ContextEvent, + ContextEventResult, + ContextUsage, + Extension, + ExtensionActions, + ExtensionCommandContext, + ExtensionCommandContextActions, + ExtensionContext, + ExtensionContextActions, + ExtensionError, + ExtensionEvent, + ExtensionFlag, + ExtensionRuntime, + ExtensionShortcut, + ExtensionUIContext, + InputEvent, + InputEventResult, + InputSource, + MessageEndEvent, + MessageEndEventResult, + MessageRenderer, + ProviderConfig, + RegisteredCommand, + RegisteredTool, + ReplacedSessionContext, + ResolvedCommand, + ResourcesDiscoverEvent, + ResourcesDiscoverResult, + SessionBeforeCompactResult, + SessionBeforeForkResult, + SessionBeforeSwitchResult, + SessionBeforeTreeResult, + SessionShutdownEvent, + ToolCallEvent, + ToolCallEventResult, + ToolResultEvent, + ToolResultEventResult, + UserBashEvent, + UserBashEventResult, +} from "./types.js"; + +// Extension shortcuts compete with canonical keybinding ids from keybindings.json. +// Only editor-global shortcuts are reserved here. Picker-specific bindings are not. +const RESERVED_KEYBINDINGS_FOR_EXTENSION_CONFLICTS = [ + "app.interrupt", + "app.clear", + "app.exit", + "app.suspend", + "app.thinking.cycle", + "app.model.cycleForward", + "app.model.cycleBackward", + "app.model.select", + "app.tools.expand", + "app.thinking.toggle", + "app.editor.external", + "app.message.followUp", + "tui.input.submit", + "tui.select.confirm", + "tui.select.cancel", + "tui.input.copy", + "tui.editor.deleteToLineEnd", +] as const; + +type BuiltInKeyBindings = Partial>; + +const buildBuiltinKeybindings = (resolvedKeybindings: KeybindingsConfig): BuiltInKeyBindings => { + const builtinKeybindings = {} as BuiltInKeyBindings; + for (const [keybinding, keys] of Object.entries(resolvedKeybindings)) { + if (keys === undefined) { + continue; + } + const keyList = Array.isArray(keys) ? keys : [keys]; + const restrictOverride = ( + RESERVED_KEYBINDINGS_FOR_EXTENSION_CONFLICTS as readonly string[] + ).includes(keybinding); + for (const key of keyList) { + const normalizedKey = key.toLowerCase() as KeyId; + // If multiple actions bind the same key, the reserved action wins so extensions + // remain blocked by reserved shortcuts regardless of iteration order. + const existing = builtinKeybindings[normalizedKey]; + if (existing?.restrictOverride && !restrictOverride) { + continue; + } + builtinKeybindings[normalizedKey] = { + keybinding, + restrictOverride, + }; + } + } + return builtinKeybindings; +}; + +/** Combined result from all before_agent_start handlers */ +interface BeforeAgentStartCombinedResult { + messages?: NonNullable[]; + systemPrompt?: string; +} + +/** + * Events handled by the generic emit() method. + * Events with dedicated emitXxx() methods are excluded for stronger type safety. + */ +type RunnerEmitEvent = Exclude< + ExtensionEvent, + | ToolCallEvent + | ToolResultEvent + | UserBashEvent + | ContextEvent + | BeforeProviderRequestEvent + | BeforeAgentStartEvent + | MessageEndEvent + | ResourcesDiscoverEvent + | InputEvent +>; + +type SessionBeforeEvent = Extract< + RunnerEmitEvent, + { + type: + | "session_before_switch" + | "session_before_fork" + | "session_before_compact" + | "session_before_tree"; + } +>; + +type SessionBeforeEventResult = + | SessionBeforeSwitchResult + | SessionBeforeForkResult + | SessionBeforeCompactResult + | SessionBeforeTreeResult; + +type RunnerEmitResult = TEvent extends { + type: "session_before_switch"; +} + ? SessionBeforeSwitchResult | undefined + : TEvent extends { type: "session_before_fork" } + ? SessionBeforeForkResult | undefined + : TEvent extends { type: "session_before_compact" } + ? SessionBeforeCompactResult | undefined + : TEvent extends { type: "session_before_tree" } + ? SessionBeforeTreeResult | undefined + : undefined; + +export type ExtensionErrorListener = (error: ExtensionError) => void; + +export type NewSessionHandler = (options?: { + parentSession?: string; + setup?: (sessionManager: SessionManager) => Promise; + withSession?: (ctx: ReplacedSessionContext) => Promise; +}) => Promise<{ cancelled: boolean }>; + +export type ForkHandler = ( + entryId: string, + options?: { + position?: "before" | "at"; + withSession?: (ctx: ReplacedSessionContext) => Promise; + }, +) => Promise<{ cancelled: boolean }>; + +export type NavigateTreeHandler = ( + targetId: string, + options?: { + summarize?: boolean; + customInstructions?: string; + replaceInstructions?: boolean; + label?: string; + }, +) => Promise<{ cancelled: boolean }>; + +export type SwitchSessionHandler = ( + sessionPath: string, + options?: { withSession?: (ctx: ReplacedSessionContext) => Promise }, +) => Promise<{ cancelled: boolean }>; + +export type ReloadHandler = () => Promise; + +export type ShutdownHandler = () => void; + +/** + * Helper function to emit session_shutdown event to extensions. + * Returns true if the event was emitted, false if there were no handlers. + */ +export async function emitSessionShutdownEvent( + extensionRunner: ExtensionRunner, + event: SessionShutdownEvent, +): Promise { + if (extensionRunner.hasHandlers("session_shutdown")) { + await extensionRunner.emit(event); + return true; + } + return false; +} + +const noOpUIContext: ExtensionUIContext = { + select: async () => undefined, + confirm: async () => false, + input: async () => undefined, + notify: () => {}, + onTerminalInput: () => () => {}, + setStatus: () => {}, + setWorkingMessage: () => {}, + setWorkingVisible: () => {}, + setWorkingIndicator: () => {}, + setHiddenThinkingLabel: () => {}, + setWidget: () => {}, + setFooter: () => {}, + setHeader: () => {}, + setTitle: () => {}, + custom: async () => undefined as never, + pasteToEditor: () => {}, + setEditorText: () => {}, + getEditorText: () => "", + editor: async () => undefined, + addAutocompleteProvider: () => {}, + setEditorComponent: () => {}, + getEditorComponent: () => undefined, + get theme() { + return theme; + }, + getAllThemes: () => [], + getTheme: () => undefined, + setTheme: (nextTheme: string | Theme) => { + void nextTheme; + return { success: false, error: "UI not available" }; + }, + getToolsExpanded: () => false, + setToolsExpanded: () => {}, +}; + +export class ExtensionRunner { + private extensions: Extension[]; + private runtime: ExtensionRuntime; + private uiContext: ExtensionUIContext; + private cwd: string; + private sessionManager: SessionManager; + private modelRegistry: ModelRegistry; + private errorListeners: Set = new Set(); + private getModel: () => Model | undefined = () => undefined; + private isIdleFn: () => boolean = () => true; + private getSignalFn: () => AbortSignal | undefined = () => undefined; + private waitForIdleFn: () => Promise = async () => {}; + private abortFn: () => void = () => {}; + private hasPendingMessagesFn: () => boolean = () => false; + private getContextUsageFn: () => ContextUsage | undefined = () => undefined; + private compactFn: (options?: CompactOptions) => void = () => {}; + private getSystemPromptFn: () => string = () => ""; + private newSessionHandler: NewSessionHandler = async () => ({ cancelled: false }); + private forkHandler: ForkHandler = async () => ({ cancelled: false }); + private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false }); + private switchSessionHandler: SwitchSessionHandler = async () => ({ cancelled: false }); + private reloadHandler: ReloadHandler = async () => {}; + private shutdownHandler: ShutdownHandler = () => {}; + private shortcutDiagnostics: ResourceDiagnostic[] = []; + private commandDiagnostics: ResourceDiagnostic[] = []; + private staleMessage: string | undefined; + + constructor( + extensions: Extension[], + runtime: ExtensionRuntime, + cwd: string, + sessionManager: SessionManager, + modelRegistry: ModelRegistry, + ) { + this.extensions = extensions; + this.runtime = runtime; + this.uiContext = noOpUIContext; + this.cwd = cwd; + this.sessionManager = sessionManager; + this.modelRegistry = modelRegistry; + } + + bindCore( + actions: ExtensionActions, + contextActions: ExtensionContextActions, + providerActions?: { + registerProvider?: (name: string, config: ProviderConfig) => void; + unregisterProvider?: (name: string) => void; + }, + ): void { + // Copy actions into the shared runtime (all extension APIs reference this) + this.runtime.sendMessage = actions.sendMessage; + this.runtime.sendUserMessage = actions.sendUserMessage; + this.runtime.appendEntry = actions.appendEntry; + this.runtime.setSessionName = actions.setSessionName; + this.runtime.getSessionName = actions.getSessionName; + this.runtime.setLabel = actions.setLabel; + this.runtime.getActiveTools = actions.getActiveTools; + this.runtime.getAllTools = actions.getAllTools; + this.runtime.setActiveTools = actions.setActiveTools; + this.runtime.refreshTools = actions.refreshTools; + this.runtime.getCommands = actions.getCommands; + this.runtime.setModel = actions.setModel; + this.runtime.getThinkingLevel = actions.getThinkingLevel; + this.runtime.setThinkingLevel = actions.setThinkingLevel; + + // Context actions (required) + this.getModel = contextActions.getModel; + this.isIdleFn = contextActions.isIdle; + this.getSignalFn = contextActions.getSignal; + this.abortFn = contextActions.abort; + this.hasPendingMessagesFn = contextActions.hasPendingMessages; + this.shutdownHandler = contextActions.shutdown; + this.getContextUsageFn = contextActions.getContextUsage; + this.compactFn = contextActions.compact; + this.getSystemPromptFn = contextActions.getSystemPrompt; + + // Flush provider registrations queued during extension loading + for (const { name, config, extensionPath } of this.runtime.pendingProviderRegistrations) { + try { + if (providerActions?.registerProvider) { + providerActions.registerProvider(name, config); + } else { + this.modelRegistry.registerProvider(name, config); + } + } catch (err) { + this.emitError({ + extensionPath, + event: "register_provider", + error: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack : undefined, + }); + } + } + this.runtime.pendingProviderRegistrations = []; + + // From this point on, provider registration/unregistration takes effect immediately + // without requiring a /reload. + this.runtime.registerProvider = (name, config) => { + if (providerActions?.registerProvider) { + providerActions.registerProvider(name, config); + return; + } + this.modelRegistry.registerProvider(name, config); + }; + this.runtime.unregisterProvider = (name) => { + if (providerActions?.unregisterProvider) { + providerActions.unregisterProvider(name); + return; + } + this.modelRegistry.unregisterProvider(name); + }; + } + + bindCommandContext(actions?: ExtensionCommandContextActions): void { + if (actions) { + this.waitForIdleFn = actions.waitForIdle; + this.newSessionHandler = actions.newSession; + this.forkHandler = actions.fork; + this.navigateTreeHandler = actions.navigateTree; + this.switchSessionHandler = actions.switchSession; + this.reloadHandler = actions.reload; + return; + } + + this.waitForIdleFn = async () => {}; + this.newSessionHandler = async () => ({ cancelled: false }); + this.forkHandler = async () => ({ cancelled: false }); + this.navigateTreeHandler = async () => ({ cancelled: false }); + this.switchSessionHandler = async () => ({ cancelled: false }); + this.reloadHandler = async () => {}; + } + + setUIContext(uiContext?: ExtensionUIContext): void { + this.uiContext = uiContext ?? noOpUIContext; + } + + getUIContext(): ExtensionUIContext { + return this.uiContext; + } + + hasUI(): boolean { + return this.uiContext !== noOpUIContext; + } + + getExtensionPaths(): string[] { + return this.extensions.map((e) => e.path); + } + + /** Get all registered tools from all extensions (first registration per name wins). */ + getAllRegisteredTools(): RegisteredTool[] { + const toolsByName = new Map(); + for (const ext of this.extensions) { + for (const tool of ext.tools.values()) { + if (!toolsByName.has(tool.definition.name)) { + toolsByName.set(tool.definition.name, tool); + } + } + } + return Array.from(toolsByName.values()); + } + + /** Get a tool definition by name. Returns undefined if not found. */ + getToolDefinition(toolName: string): RegisteredTool["definition"] | undefined { + for (const ext of this.extensions) { + const tool = ext.tools.get(toolName); + if (tool) { + return tool.definition; + } + } + return undefined; + } + + getFlags(): Map { + const allFlags = new Map(); + for (const ext of this.extensions) { + for (const [name, flag] of ext.flags) { + if (!allFlags.has(name)) { + allFlags.set(name, flag); + } + } + } + return allFlags; + } + + setFlagValue(name: string, value: boolean | string): void { + this.runtime.flagValues.set(name, value); + } + + getFlagValues(): Map { + return new Map(this.runtime.flagValues); + } + + getShortcuts(resolvedKeybindings: KeybindingsConfig): Map { + this.shortcutDiagnostics = []; + const builtinKeybindings = buildBuiltinKeybindings(resolvedKeybindings); + const extensionShortcuts = new Map(); + + const addDiagnostic = (message: string, extensionPath: string) => { + this.shortcutDiagnostics.push({ type: "warning", message, path: extensionPath }); + if (!this.hasUI()) { + console.warn(message); + } + }; + + for (const ext of this.extensions) { + for (const [key, shortcut] of ext.shortcuts) { + const normalizedKey = key.toLowerCase() as KeyId; + + const builtInKeybinding = builtinKeybindings[normalizedKey]; + if (builtInKeybinding?.restrictOverride === true) { + addDiagnostic( + `Extension shortcut '${key}' from ${shortcut.extensionPath} conflicts with built-in shortcut. Skipping.`, + shortcut.extensionPath, + ); + continue; + } + + if (builtInKeybinding?.restrictOverride === false) { + addDiagnostic( + `Extension shortcut conflict: '${key}' is built-in shortcut for ${builtInKeybinding.keybinding} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`, + shortcut.extensionPath, + ); + } + + const existingExtensionShortcut = extensionShortcuts.get(normalizedKey); + if (existingExtensionShortcut) { + addDiagnostic( + `Extension shortcut conflict: '${key}' registered by both ${existingExtensionShortcut.extensionPath} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`, + shortcut.extensionPath, + ); + } + extensionShortcuts.set(normalizedKey, shortcut); + } + } + return extensionShortcuts; + } + + getShortcutDiagnostics(): ResourceDiagnostic[] { + return this.shortcutDiagnostics; + } + + invalidate( + message = "This extension ctx is stale after session replacement or reload. Do not use a captured api or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().", + ): void { + if (!this.staleMessage) { + this.staleMessage = message; + this.runtime.invalidate(message); + } + } + + private assertActive(): void { + if (this.staleMessage) { + throw new Error(this.staleMessage); + } + } + + onError(listener: ExtensionErrorListener): () => void { + this.errorListeners.add(listener); + return () => this.errorListeners.delete(listener); + } + + emitError(error: ExtensionError): void { + for (const listener of this.errorListeners) { + listener(error); + } + } + + hasHandlers(eventType: string): boolean { + for (const ext of this.extensions) { + const handlers = ext.handlers.get(eventType); + if (handlers && handlers.length > 0) { + return true; + } + } + return false; + } + + getMessageRenderer(customType: string): MessageRenderer | undefined { + for (const ext of this.extensions) { + const renderer = ext.messageRenderers.get(customType); + if (renderer) { + return renderer; + } + } + return undefined; + } + + private resolveRegisteredCommands(): ResolvedCommand[] { + const commands: RegisteredCommand[] = []; + const counts = new Map(); + + for (const ext of this.extensions) { + for (const command of ext.commands.values()) { + commands.push(command); + counts.set(command.name, (counts.get(command.name) ?? 0) + 1); + } + } + + const seen = new Map(); + const takenInvocationNames = new Set(); + + return commands.map((command) => { + const occurrence = (seen.get(command.name) ?? 0) + 1; + seen.set(command.name, occurrence); + + let invocationName = + (counts.get(command.name) ?? 0) > 1 ? `${command.name}:${occurrence}` : command.name; + + if (takenInvocationNames.has(invocationName)) { + let suffix = occurrence; + do { + suffix++; + invocationName = `${command.name}:${suffix}`; + } while (takenInvocationNames.has(invocationName)); + } + + takenInvocationNames.add(invocationName); + return Object.assign({}, command, { invocationName }); + }); + } + + getRegisteredCommands(): ResolvedCommand[] { + this.commandDiagnostics = []; + return this.resolveRegisteredCommands(); + } + + getCommandDiagnostics(): ResourceDiagnostic[] { + return this.commandDiagnostics; + } + + getCommand(name: string): ResolvedCommand | undefined { + return this.resolveRegisteredCommands().find((command) => command.invocationName === name); + } + + /** + * Request a graceful shutdown. Called by extension tools and event handlers. + * The actual shutdown behavior is provided by the mode via bindExtensions(). + */ + shutdown(): void { + this.shutdownHandler(); + } + + /** + * Create an ExtensionContext for use in event handlers and tool execution. + * Context values are resolved at call time, so changes via bindCore/bindUI are reflected. + */ + createContext(): ExtensionContext { + const assertActive = () => this.assertActive(); + const getModel = this.getModel; + const getUiContext = () => this.uiContext; + const hasUiContext = () => this.hasUI(); + const getCwd = () => this.cwd; + const getSessionManager = () => this.sessionManager; + const getModelRegistry = () => this.modelRegistry; + const isIdle = () => this.isIdleFn(); + const getSignal = () => this.getSignalFn(); + const abort = () => this.abortFn(); + const hasPendingMessages = () => this.hasPendingMessagesFn(); + const shutdown = () => this.shutdownHandler(); + const getContextUsage = () => this.getContextUsageFn(); + const compact = (options?: CompactOptions) => this.compactFn(options); + const getSystemPrompt = () => this.getSystemPromptFn(); + return { + get ui() { + assertActive(); + return getUiContext(); + }, + get hasUI() { + assertActive(); + return hasUiContext(); + }, + get cwd() { + assertActive(); + return getCwd(); + }, + get sessionManager() { + assertActive(); + return getSessionManager(); + }, + get modelRegistry() { + assertActive(); + return getModelRegistry(); + }, + get model() { + assertActive(); + return getModel(); + }, + isIdle: () => { + assertActive(); + return isIdle(); + }, + get signal() { + assertActive(); + return getSignal(); + }, + abort: () => { + assertActive(); + abort(); + }, + hasPendingMessages: () => { + assertActive(); + return hasPendingMessages(); + }, + shutdown: () => { + assertActive(); + shutdown(); + }, + getContextUsage: () => { + assertActive(); + return getContextUsage(); + }, + compact: (options) => { + assertActive(); + compact(options); + }, + getSystemPrompt: () => { + assertActive(); + return getSystemPrompt(); + }, + }; + } + + createCommandContext(): ExtensionCommandContext { + // Use property descriptors instead of object spread so the guarded getters from + // createContext() stay lazy. A spread would eagerly read them once and freeze the + // old values into the returned object, bypassing stale-instance checks. + const context = Object.defineProperties( + {}, + Object.getOwnPropertyDescriptors(this.createContext()), + ) as ExtensionCommandContext; + context.waitForIdle = () => { + this.assertActive(); + return this.waitForIdleFn(); + }; + context.newSession = (options) => { + this.assertActive(); + return this.newSessionHandler(options); + }; + context.fork = (entryId, options) => { + this.assertActive(); + return this.forkHandler(entryId, options); + }; + context.navigateTree = (targetId, options) => { + this.assertActive(); + return this.navigateTreeHandler(targetId, options); + }; + context.switchSession = (sessionPath, options) => { + this.assertActive(); + return this.switchSessionHandler(sessionPath, options); + }; + context.reload = () => { + this.assertActive(); + return this.reloadHandler(); + }; + return context; + } + + private isSessionBeforeEvent(event: RunnerEmitEvent): event is SessionBeforeEvent { + return ( + event.type === "session_before_switch" || + event.type === "session_before_fork" || + event.type === "session_before_compact" || + event.type === "session_before_tree" + ); + } + + async emit(event: TEvent): Promise> { + const ctx = this.createContext(); + let result: SessionBeforeEventResult | undefined; + + for (const ext of this.extensions) { + const handlers = ext.handlers.get(event.type); + if (!handlers || handlers.length === 0) { + continue; + } + + for (const handler of handlers) { + try { + const handlerResult = await handler(event, ctx); + + if (this.isSessionBeforeEvent(event) && handlerResult) { + result = handlerResult as SessionBeforeEventResult; + if (result.cancel) { + return result as RunnerEmitResult; + } + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; + this.emitError({ + extensionPath: ext.path, + event: event.type, + error: message, + stack, + }); + } + } + } + + return result as RunnerEmitResult; + } + + async emitMessageEnd(event: MessageEndEvent): Promise { + const ctx = this.createContext(); + let currentMessage = event.message; + let modified = false; + + for (const ext of this.extensions) { + const handlers = ext.handlers.get("message_end"); + if (!handlers || handlers.length === 0) { + continue; + } + + for (const handler of handlers) { + try { + const currentEvent: MessageEndEvent = { ...event, message: currentMessage }; + const handlerResult = (await handler(currentEvent, ctx)) as + | MessageEndEventResult + | undefined; + if (!handlerResult?.message) { + continue; + } + + if (handlerResult.message.role !== currentMessage.role) { + this.emitError({ + extensionPath: ext.path, + event: "message_end", + error: "message_end handlers must return a message with the same role", + }); + continue; + } + + currentMessage = handlerResult.message; + modified = true; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; + this.emitError({ + extensionPath: ext.path, + event: "message_end", + error: message, + stack, + }); + } + } + } + + return modified ? currentMessage : undefined; + } + + async emitToolResult(event: ToolResultEvent): Promise { + const ctx = this.createContext(); + const currentEvent: ToolResultEvent = { ...event }; + let modified = false; + + for (const ext of this.extensions) { + const handlers = ext.handlers.get("tool_result"); + if (!handlers || handlers.length === 0) { + continue; + } + + for (const handler of handlers) { + try { + const handlerResult = (await handler(currentEvent, ctx)) as + | ToolResultEventResult + | undefined; + if (!handlerResult) { + continue; + } + + if (handlerResult.content !== undefined) { + currentEvent.content = handlerResult.content; + modified = true; + } + if (handlerResult.details !== undefined) { + currentEvent.details = handlerResult.details; + modified = true; + } + if (handlerResult.isError !== undefined) { + currentEvent.isError = handlerResult.isError; + modified = true; + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; + this.emitError({ + extensionPath: ext.path, + event: "tool_result", + error: message, + stack, + }); + } + } + } + + if (!modified) { + return undefined; + } + + return { + content: currentEvent.content, + details: currentEvent.details, + isError: currentEvent.isError, + }; + } + + async emitToolCall(event: ToolCallEvent): Promise { + const ctx = this.createContext(); + let result: ToolCallEventResult | undefined; + + for (const ext of this.extensions) { + const handlers = ext.handlers.get("tool_call"); + if (!handlers || handlers.length === 0) { + continue; + } + + for (const handler of handlers) { + const handlerResult = await handler(event, ctx); + + if (handlerResult) { + result = handlerResult as ToolCallEventResult; + if (result.block) { + return result; + } + } + } + } + + return result; + } + + async emitUserBash(event: UserBashEvent): Promise { + const ctx = this.createContext(); + + for (const ext of this.extensions) { + const handlers = ext.handlers.get("user_bash"); + if (!handlers || handlers.length === 0) { + continue; + } + + for (const handler of handlers) { + try { + const handlerResult = await handler(event, ctx); + if (handlerResult) { + return handlerResult as UserBashEventResult; + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; + this.emitError({ + extensionPath: ext.path, + event: "user_bash", + error: message, + stack, + }); + } + } + } + + return undefined; + } + + async emitContext(messages: AgentMessage[]): Promise { + const ctx = this.createContext(); + let currentMessages = structuredClone(messages); + + for (const ext of this.extensions) { + const handlers = ext.handlers.get("context"); + if (!handlers || handlers.length === 0) { + continue; + } + + for (const handler of handlers) { + try { + const event: ContextEvent = { type: "context", messages: currentMessages }; + const handlerResult = await handler(event, ctx); + + if (handlerResult && (handlerResult as ContextEventResult).messages) { + currentMessages = (handlerResult as ContextEventResult).messages!; + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; + this.emitError({ + extensionPath: ext.path, + event: "context", + error: message, + stack, + }); + } + } + } + + return currentMessages; + } + + async emitBeforeProviderRequest(payload: unknown): Promise { + const ctx = this.createContext(); + let currentPayload = payload; + + for (const ext of this.extensions) { + const handlers = ext.handlers.get("before_provider_request"); + if (!handlers || handlers.length === 0) { + continue; + } + + for (const handler of handlers) { + try { + const event: BeforeProviderRequestEvent = { + type: "before_provider_request", + payload: currentPayload, + }; + const handlerResult = await handler(event, ctx); + if (handlerResult !== undefined) { + currentPayload = handlerResult; + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; + this.emitError({ + extensionPath: ext.path, + event: "before_provider_request", + error: message, + stack, + }); + } + } + } + + return currentPayload; + } + + async emitBeforeAgentStart( + prompt: string, + images: ImageContent[] | undefined, + systemPrompt: string, + systemPromptOptions: BuildSystemPromptOptions, + ): Promise { + let currentSystemPrompt = systemPrompt; + const ctx = Object.defineProperties( + {}, + Object.getOwnPropertyDescriptors(this.createContext()), + ) as ExtensionContext; + ctx.getSystemPrompt = () => { + this.assertActive(); + return currentSystemPrompt; + }; + const messages: NonNullable[] = []; + let systemPromptModified = false; + + for (const ext of this.extensions) { + const handlers = ext.handlers.get("before_agent_start"); + if (!handlers || handlers.length === 0) { + continue; + } + + for (const handler of handlers) { + try { + const event: BeforeAgentStartEvent = { + type: "before_agent_start", + prompt, + images, + systemPrompt: currentSystemPrompt, + systemPromptOptions, + }; + const handlerResult = await handler(event, ctx); + + if (handlerResult) { + const result = handlerResult as BeforeAgentStartEventResult; + if (result.message) { + messages.push(result.message); + } + if (result.systemPrompt !== undefined) { + currentSystemPrompt = result.systemPrompt; + systemPromptModified = true; + } + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; + this.emitError({ + extensionPath: ext.path, + event: "before_agent_start", + error: message, + stack, + }); + } + } + } + + if (messages.length > 0 || systemPromptModified) { + return { + messages: messages.length > 0 ? messages : undefined, + systemPrompt: systemPromptModified ? currentSystemPrompt : undefined, + }; + } + + return undefined; + } + + async emitResourcesDiscover( + cwd: string, + reason: ResourcesDiscoverEvent["reason"], + ): Promise<{ + skillPaths: Array<{ path: string; extensionPath: string }>; + promptPaths: Array<{ path: string; extensionPath: string }>; + themePaths: Array<{ path: string; extensionPath: string }>; + }> { + const ctx = this.createContext(); + const skillPaths: Array<{ path: string; extensionPath: string }> = []; + const promptPaths: Array<{ path: string; extensionPath: string }> = []; + const themePaths: Array<{ path: string; extensionPath: string }> = []; + + for (const ext of this.extensions) { + const handlers = ext.handlers.get("resources_discover"); + if (!handlers || handlers.length === 0) { + continue; + } + + for (const handler of handlers) { + try { + const event: ResourcesDiscoverEvent = { type: "resources_discover", cwd, reason }; + const handlerResult = await handler(event, ctx); + const result = handlerResult as ResourcesDiscoverResult | undefined; + + if (result?.skillPaths?.length) { + skillPaths.push( + ...result.skillPaths.map((path) => ({ path, extensionPath: ext.path })), + ); + } + if (result?.promptPaths?.length) { + promptPaths.push( + ...result.promptPaths.map((path) => ({ path, extensionPath: ext.path })), + ); + } + if (result?.themePaths?.length) { + themePaths.push( + ...result.themePaths.map((path) => ({ path, extensionPath: ext.path })), + ); + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; + this.emitError({ + extensionPath: ext.path, + event: "resources_discover", + error: message, + stack, + }); + } + } + } + + return { skillPaths, promptPaths, themePaths }; + } + + /** Emit input event. Transforms chain, "handled" short-circuits. */ + async emitInput( + text: string, + images: ImageContent[] | undefined, + source: InputSource, + ): Promise { + const ctx = this.createContext(); + let currentText = text; + let currentImages = images; + + for (const ext of this.extensions) { + for (const handler of ext.handlers.get("input") ?? []) { + try { + const event: InputEvent = { + type: "input", + text: currentText, + images: currentImages, + source, + }; + const result = (await handler(event, ctx)) as InputEventResult | undefined; + if (result?.action === "handled") { + return result; + } + if (result?.action === "transform") { + currentText = result.text; + currentImages = result.images ?? currentImages; + } + } catch (err) { + this.emitError({ + extensionPath: ext.path, + event: "input", + error: err instanceof Error ? err.message : String(err), + stack: err instanceof Error ? err.stack : undefined, + }); + } + } + } + return currentText !== text || currentImages !== images + ? { action: "transform", text: currentText, images: currentImages } + : { action: "continue" }; + } +} diff --git a/src/agents/sessions/extensions/types.ts b/src/agents/sessions/extensions/types.ts new file mode 100644 index 00000000000..843da16fd87 --- /dev/null +++ b/src/agents/sessions/extensions/types.ts @@ -0,0 +1,1660 @@ +/** + * Extension system types. + * + * Extensions are TypeScript modules that can: + * - Subscribe to agent lifecycle events + * - Register LLM-callable tools + * - Register commands, keyboard shortcuts, and CLI flags + * - Interact with the user via UI primitives + */ + +import type { + AutocompleteItem, + AutocompleteProvider, + Component, + EditorComponent, + EditorTheme, + KeyId, + OverlayHandle, + OverlayOptions, + TUI, +} from "@earendil-works/pi-tui"; +import type { + Api, + AssistantMessageEvent, + AssistantMessageEventStream, + Context, + ImageContent, + Model, + OAuthCredentials, + OAuthLoginCallbacks, + SimpleStreamOptions, + TextContent, + ToolResultMessage, +} from "openclaw/plugin-sdk/llm"; +import type { Static, TSchema } from "typebox"; +import type { Theme } from "../../modes/interactive/theme/theme.js"; +import type { + AgentMessage, + AgentToolResult, + AgentToolUpdateCallback, + ThinkingLevel, + ToolExecutionMode, +} from "../../runtime/index.js"; +import type { BashResult } from "../bash-executor.js"; +import type { CompactionPreparation, CompactionResult } from "../compaction/index.js"; +import type { EventBus } from "../event-bus.js"; +import type { ExecOptions, ExecResult } from "../exec.js"; +import type { ReadonlyFooterDataProvider } from "../footer-data-provider.js"; +import type { KeybindingsManager } from "../keybindings.js"; +import type { CustomMessage } from "../messages.js"; +import type { ModelRegistry } from "../model-registry.js"; +import type { + BranchSummaryEntry, + CompactionEntry, + ReadonlySessionManager, + SessionEntry, + SessionManager, +} from "../session-manager.js"; +import type { SlashCommandInfo } from "../slash-commands.js"; +import type { SourceInfo } from "../source-info.js"; +import type { BuildSystemPromptOptions } from "../system-prompt.js"; +import type { BashOperations } from "../tools/bash.js"; +import type { EditToolDetails } from "../tools/edit.js"; +import type { + BashToolDetails, + BashToolInput, + EditToolInput, + FindToolDetails, + FindToolInput, + GrepToolDetails, + GrepToolInput, + LsToolDetails, + LsToolInput, + ReadToolDetails, + ReadToolInput, + WriteToolInput, +} from "../tools/index.js"; + +export type { ExecOptions, ExecResult } from "../exec.js"; +export type { BuildSystemPromptOptions } from "../system-prompt.js"; +export type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode }; +export type { AppKeybinding, KeybindingsManager } from "../keybindings.js"; + +// ============================================================================ +// UI Context +// ============================================================================ + +/** Options for extension UI dialogs. */ +export interface ExtensionUIDialogOptions { + /** AbortSignal to programmatically dismiss the dialog. */ + signal?: AbortSignal; + /** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */ + timeout?: number; +} + +/** Placement for extension widgets. */ +export type WidgetPlacement = "aboveEditor" | "belowEditor"; + +/** Options for extension widgets. */ +export interface ExtensionWidgetOptions { + /** Where the widget is rendered. Defaults to "aboveEditor". */ + placement?: WidgetPlacement; +} + +/** Raw terminal input listener for extensions. */ +export type TerminalInputHandler = ( + data: string, +) => { consume?: boolean; data?: string } | undefined; + +/** Working indicator configuration for the interactive streaming loader. */ +export interface WorkingIndicatorOptions { + /** Animation frames. Use an empty array to hide the indicator entirely. Custom frames are rendered verbatim. */ + frames?: string[]; + /** Frame interval in milliseconds for animated indicators. */ + intervalMs?: number; +} + +/** Wrap the current autocomplete provider with additional behavior. */ +export type AutocompleteProviderFactory = (current: AutocompleteProvider) => AutocompleteProvider; +export type EditorFactory = ( + tui: TUI, + theme: EditorTheme, + keybindings: KeybindingsManager, +) => EditorComponent; + +/** + * UI context for extensions to request interactive UI. + * Each mode (interactive, RPC, print) provides its own implementation. + */ +export interface ExtensionUIContext { + /** Show a selector and return the user's choice. */ + select( + title: string, + options: string[], + opts?: ExtensionUIDialogOptions, + ): Promise; + + /** Show a confirmation dialog. */ + confirm(title: string, message: string, opts?: ExtensionUIDialogOptions): Promise; + + /** Show a text input dialog. */ + input( + title: string, + placeholder?: string, + opts?: ExtensionUIDialogOptions, + ): Promise; + + /** Show a notification to the user. */ + notify(message: string, type?: "info" | "warning" | "error"): void; + + /** Listen to raw terminal input (interactive mode only). Returns an unsubscribe function. */ + onTerminalInput(handler: TerminalInputHandler): () => void; + + /** Set status text in the footer/status bar. Pass undefined to clear. */ + setStatus(key: string, text: string | undefined): void; + + /** Set the working/loading message shown during streaming. Call with no argument to restore default. */ + setWorkingMessage(message?: string): void; + + /** Show or hide the built-in interactive working loader row during streaming. */ + setWorkingVisible(visible: boolean): void; + + /** + * Configure the interactive working indicator shown during streaming. + * + * - Omit the argument to restore the default animated spinner. + * - Use `frames: ["●"]` for a static indicator. + * - Use `frames: []` to hide the indicator entirely. + * - Custom frames are rendered as provided, so extensions must add their own colors. + */ + setWorkingIndicator(options?: WorkingIndicatorOptions): void; + + /** Set the label shown for hidden thinking blocks. Call with no argument to restore default. */ + setHiddenThinkingLabel(label?: string): void; + + /** Set a widget to display above or below the editor. Accepts string array or component factory. */ + setWidget(key: string, content: string[] | undefined, options?: ExtensionWidgetOptions): void; + setWidget( + key: string, + content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined, + options?: ExtensionWidgetOptions, + ): void; + + /** Set a custom footer component, or undefined to restore the built-in footer. + * + * The factory receives a FooterDataProvider for data not otherwise accessible: + * git branch and extension statuses from setStatus(). Token stats, model info, + * etc. are available via ctx.sessionManager and ctx.model. + */ + setFooter( + factory: + | (( + tui: TUI, + theme: Theme, + footerData: ReadonlyFooterDataProvider, + ) => Component & { dispose?(): void }) + | undefined, + ): void; + + /** Set a custom header component (shown at startup, above chat), or undefined to restore the built-in header. */ + setHeader( + factory: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined, + ): void; + + /** Set the terminal window/tab title. */ + setTitle(title: string): void; + + /** Show a custom component with keyboard focus. */ + custom( + factory: ( + tui: TUI, + theme: Theme, + keybindings: KeybindingsManager, + done: (result: T) => void, + ) => (Component & { dispose?(): void }) | Promise, + options?: { + overlay?: boolean; + /** Overlay positioning/sizing options. Can be static or a function for dynamic updates. */ + overlayOptions?: OverlayOptions | (() => OverlayOptions); + /** Called with the overlay handle after the overlay is shown. Use to control visibility. */ + onHandle?: (handle: OverlayHandle) => void; + }, + ): Promise; + + /** Paste text into the editor, triggering paste handling (collapse for large content). */ + pasteToEditor(text: string): void; + + /** Set the text in the core input editor. */ + setEditorText(text: string): void; + + /** Get the current text from the core input editor. */ + getEditorText(): string; + + /** Show a multi-line editor for text editing. */ + editor(title: string, prefill?: string): Promise; + + /** Stack additional autocomplete behavior on top of the built-in provider. */ + addAutocompleteProvider(factory: AutocompleteProviderFactory): void; + + /** + * Set a custom editor component via factory function. + * Pass undefined to restore the default editor. + * + * The factory receives: + * - `theme`: EditorTheme for styling borders and autocomplete + * - `keybindings`: KeybindingsManager for app-level keybindings + * + * For full app keybinding support (escape, ctrl+d, model switching, etc.), + * extend `CustomEditor` from `openclaw/plugin-sdk/agent-sessions` and call + * `super.handleInput(data)` for keys you don't handle. + * + * @example + * ```ts + * import { CustomEditor } from "openclaw/plugin-sdk/agent-sessions"; + * + * class VimEditor extends CustomEditor { + * private mode: "normal" | "insert" = "insert"; + * + * handleInput(data: string): void { + * if (this.mode === "normal") { + * // Handle vim normal mode keys... + * if (data === "i") { this.mode = "insert"; return; } + * } + * super.handleInput(data); // App keybindings + text editing + * } + * } + * + * ctx.ui.setEditorComponent((tui, theme, keybindings) => + * new VimEditor(tui, theme, keybindings) + * ); + * ``` + */ + setEditorComponent(factory: EditorFactory | undefined): void; + + /** Get the currently configured custom editor factory, or undefined when using the default editor. */ + getEditorComponent(): EditorFactory | undefined; + + /** Get the current theme for styling. */ + readonly theme: Theme; + + /** Get all available themes with their names and file paths. */ + getAllThemes(): { name: string; path: string | undefined }[]; + + /** Load a theme by name without switching to it. Returns undefined if not found. */ + getTheme(name: string): Theme | undefined; + + /** Set the current theme by name or Theme object. */ + setTheme(theme: string | Theme): { success: boolean; error?: string }; + + /** Get current tool output expansion state. */ + getToolsExpanded(): boolean; + + /** Set tool output expansion state. */ + setToolsExpanded(expanded: boolean): void; +} + +// ============================================================================ +// Extension Context +// ============================================================================ + +export interface ContextUsage { + /** Estimated context tokens, or null if any (e.g. right after compaction, before next LLM response). */ + tokens: number | null; + contextWindow: number; + /** Context usage as percentage of context window, or null if tokens is unknown. */ + percent: number | null; +} + +export interface CompactOptions { + customInstructions?: string; + onComplete?: (result: CompactionResult) => void; + onError?: (error: Error) => void; +} + +/** + * Context passed to extension event handlers. + */ +export interface ExtensionContext { + /** UI methods for user interaction */ + ui: ExtensionUIContext; + /** Whether UI is available (false in print/RPC mode) */ + hasUI: boolean; + /** Current working directory */ + cwd: string; + /** Session manager (read-only) */ + sessionManager: ReadonlySessionManager; + /** Model registry for API key resolution */ + modelRegistry: ModelRegistry; + /** Current model (may be undefined) */ + model: Model | undefined; + /** Whether the agent is idle (not streaming) */ + isIdle(): boolean; + /** The current abort signal, or undefined when the agent is not streaming. */ + signal: AbortSignal | undefined; + /** Abort the current agent operation */ + abort(): void; + /** Whether there are queued messages waiting */ + hasPendingMessages(): boolean; + /** Gracefully shut down OpenClaw and exit. Available in all contexts. */ + shutdown(): void; + /** Get current context usage for the active model. */ + getContextUsage(): ContextUsage | undefined; + /** Trigger compaction without awaiting completion. */ + compact(options?: CompactOptions): void; + /** Get the current effective system prompt. */ + getSystemPrompt(): string; +} + +/** + * Extended context for command handlers. + * Includes session control methods only safe in user-initiated commands. + */ +export interface ExtensionCommandContext extends ExtensionContext { + /** Wait for the agent to finish streaming */ + waitForIdle(): Promise; + + /** Start a new session, optionally with initialization. */ + newSession(options?: { + parentSession?: string; + setup?: (sessionManager: SessionManager) => Promise; + withSession?: (ctx: ReplacedSessionContext) => Promise; + }): Promise<{ cancelled: boolean }>; + + /** Fork from a specific entry, creating a new session file. */ + fork( + entryId: string, + options?: { + position?: "before" | "at"; + withSession?: (ctx: ReplacedSessionContext) => Promise; + }, + ): Promise<{ cancelled: boolean }>; + + /** Navigate to a different point in the session tree. */ + navigateTree( + targetId: string, + options?: { + summarize?: boolean; + customInstructions?: string; + replaceInstructions?: boolean; + label?: string; + }, + ): Promise<{ cancelled: boolean }>; + + /** Switch to a different session file. */ + switchSession( + sessionPath: string, + options?: { withSession?: (ctx: ReplacedSessionContext) => Promise }, + ): Promise<{ cancelled: boolean }>; + + /** Reload extensions, skills, prompts, and themes. */ + reload(): Promise; +} + +/** + * Fresh command-capable context bound to the replacement session after a session switch. + * + * This is passed to `withSession()` callbacks on `newSession()`, `fork()`, and `switchSession()`. + */ +export interface ReplacedSessionContext extends ExtensionCommandContext { + sendMessage( + message: Pick, "customType" | "content" | "display" | "details">, + options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }, + ): Promise; + + sendUserMessage( + content: string | (TextContent | ImageContent)[], + options?: { deliverAs?: "steer" | "followUp" }, + ): Promise; +} + +// ============================================================================ +// Tool Types +// ============================================================================ + +/** Rendering options for tool results */ +export interface ToolRenderResultOptions { + /** Whether the result view is expanded */ + expanded: boolean; + /** Whether this is a partial/streaming result */ + isPartial: boolean; +} + +/** Context passed to tool renderers. */ +export interface ToolRenderContext { + /** Current tool call arguments. Shared across call/result renders for the same tool call. */ + args: TArgs; + /** Unique id for this tool execution. Stable across call/result renders for the same tool call. */ + toolCallId: string; + /** Invalidate just this tool execution component for redraw. */ + invalidate: () => void; + /** Previously returned component for this render slot, if any. */ + lastComponent: Component | undefined; + /** Shared renderer state for this tool row. Initialized by tool-execution.ts. */ + state: TState; + /** Working directory for this tool execution. */ + cwd: string; + /** Whether the tool execution has started. */ + executionStarted: boolean; + /** Whether the tool call arguments are complete. */ + argsComplete: boolean; + /** Whether the tool result is partial/streaming. */ + isPartial: boolean; + /** Whether the result view is expanded. */ + expanded: boolean; + /** Whether inline images are currently shown in the TUI. */ + showImages: boolean; + /** Whether the current result is an error. */ + isError: boolean; +} + +type BivariantCallback = { + bivarianceHack(...args: TArgs): TResult; +}["bivarianceHack"]; + +/** + * Tool definition for registerTool(). + */ +export interface ToolDefinition< + TParams extends TSchema = TSchema, + TDetails = unknown, + TState = unknown, +> { + /** Tool name (used in LLM tool calls) */ + name: string; + /** Human-readable label for UI */ + label: string; + /** Description for LLM */ + description: string; + /** Optional one-line snippet for the Available tools section in the default system prompt. Custom tools are omitted from that section when this is not provided. */ + promptSnippet?: string; + /** Optional guideline bullets appended to the default system prompt Guidelines section when this tool is active. */ + promptGuidelines?: string[]; + /** Parameter schema (TypeBox) */ + parameters: TParams; + /** Controls whether ToolExecutionComponent renders the standard colored shell or the tool renders its own framing. */ + renderShell?: "default" | "self"; + + /** Optional compatibility shim to prepare raw tool call arguments before schema validation. Must return an object conforming to TParams. */ + prepareArguments?: (args: unknown) => Static; + + /** + * Per-tool execution mode override. + * - "sequential": this tool must execute one at a time with other tool calls. + * - "parallel": this tool can execute concurrently with other tool calls. + * + * If omitted, the default execution mode applies. + */ + executionMode?: ToolExecutionMode; + + /** Execute the tool. */ + execute( + toolCallId: string, + params: Static, + signal: AbortSignal | undefined, + onUpdate: AgentToolUpdateCallback | undefined, + ctx: ExtensionContext, + ): Promise>; + + /** Custom rendering for tool call display */ + renderCall?: BivariantCallback< + [args: Static, theme: Theme, context: ToolRenderContext>], + Component + >; + + /** Custom rendering for tool result display */ + renderResult?: BivariantCallback< + [ + result: AgentToolResult, + options: ToolRenderResultOptions, + theme: Theme, + context: ToolRenderContext>, + ], + Component + >; +} + +type AnyToolDefinition = ToolDefinition; + +/** + * Preserve parameter inference for standalone tool definitions. + * + * Use this when assigning a tool to a variable or passing it through arrays such + * as `customTools`, where contextual typing would otherwise widen params to + * `unknown`. + */ +export function defineTool( + tool: ToolDefinition, +): ToolDefinition & AnyToolDefinition { + return tool as ToolDefinition & AnyToolDefinition; +} + +// ============================================================================ +// Resource Events +// ============================================================================ + +/** Fired after session_start to allow extensions to provide additional resource paths. */ +export interface ResourcesDiscoverEvent { + type: "resources_discover"; + cwd: string; + reason: "startup" | "reload"; +} + +/** Result from resources_discover event handler */ +export interface ResourcesDiscoverResult { + skillPaths?: string[]; + promptPaths?: string[]; + themePaths?: string[]; +} + +// ============================================================================ +// Session Events +// ============================================================================ + +/** Fired when a session is started, loaded, or reloaded */ +export interface SessionStartEvent { + type: "session_start"; + /** Why this session start happened. */ + reason: "startup" | "reload" | "new" | "resume" | "fork"; + /** Previously active session file. Present for "new", "resume", and "fork". */ + previousSessionFile?: string; +} + +/** Fired before switching to another session (can be cancelled) */ +export interface SessionBeforeSwitchEvent { + type: "session_before_switch"; + reason: "new" | "resume"; + targetSessionFile?: string; +} + +/** Fired before forking a session (can be cancelled) */ +export interface SessionBeforeForkEvent { + type: "session_before_fork"; + entryId: string; + position: "before" | "at"; +} + +/** Fired before context compaction (can be cancelled or customized) */ +export interface SessionBeforeCompactEvent { + type: "session_before_compact"; + preparation: CompactionPreparation; + branchEntries: SessionEntry[]; + customInstructions?: string; + signal: AbortSignal; +} + +/** Fired after context compaction */ +export interface SessionCompactEvent { + type: "session_compact"; + compactionEntry: CompactionEntry; + fromExtension: boolean; +} + +/** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */ +export interface SessionShutdownEvent { + type: "session_shutdown"; + reason: "quit" | "reload" | "new" | "resume" | "fork"; + /** Destination session file when shutting down due to session replacement. */ + targetSessionFile?: string; +} + +/** Preparation data for tree navigation */ +export interface TreePreparation { + targetId: string; + oldLeafId: string | null; + commonAncestorId: string | null; + entriesToSummarize: SessionEntry[]; + userWantsSummary: boolean; + /** Custom instructions for summarization */ + customInstructions?: string; + /** If true, customInstructions replaces the default prompt instead of being appended */ + replaceInstructions?: boolean; + /** Label to attach to the branch summary entry */ + label?: string; +} + +/** Fired before navigating in the session tree (can be cancelled) */ +export interface SessionBeforeTreeEvent { + type: "session_before_tree"; + preparation: TreePreparation; + signal: AbortSignal; +} + +/** Fired after navigating in the session tree */ +export interface SessionTreeEvent { + type: "session_tree"; + newLeafId: string | null; + oldLeafId: string | null; + summaryEntry?: BranchSummaryEntry; + fromExtension?: boolean; +} + +export type SessionEvent = + | SessionStartEvent + | SessionBeforeSwitchEvent + | SessionBeforeForkEvent + | SessionBeforeCompactEvent + | SessionCompactEvent + | SessionShutdownEvent + | SessionBeforeTreeEvent + | SessionTreeEvent; + +// ============================================================================ +// Agent Events +// ============================================================================ + +/** Fired before each LLM call. Can modify messages. */ +export interface ContextEvent { + type: "context"; + messages: AgentMessage[]; +} + +/** Fired before a provider request is sent. Can replace the payload. */ +export interface BeforeProviderRequestEvent { + type: "before_provider_request"; + payload: unknown; +} + +/** Fired after a provider response is received and before the response stream is consumed. */ +export interface AfterProviderResponseEvent { + type: "after_provider_response"; + status: number; + headers: Record; +} + +/** Fired after user submits prompt but before agent loop. */ +export interface BeforeAgentStartEvent { + type: "before_agent_start"; + /** The raw user prompt text (after expansion). */ + prompt: string; + /** Images attached to the user prompt, if any. */ + images?: ImageContent[]; + /** The fully assembled system prompt string. */ + systemPrompt: string; + /** Structured options used to build the system prompt. Extensions can inspect this without re-discovering resources. */ + systemPromptOptions: BuildSystemPromptOptions; +} + +/** Fired when an agent loop starts */ +export interface AgentStartEvent { + type: "agent_start"; +} + +/** Fired when an agent loop ends */ +export interface AgentEndEvent { + type: "agent_end"; + messages: AgentMessage[]; +} + +/** Fired at the start of each turn */ +export interface TurnStartEvent { + type: "turn_start"; + turnIndex: number; + timestamp: number; +} + +/** Fired at the end of each turn */ +export interface TurnEndEvent { + type: "turn_end"; + turnIndex: number; + message: AgentMessage; + toolResults: ToolResultMessage[]; +} + +/** Fired when a message starts (user, assistant, or toolResult) */ +export interface MessageStartEvent { + type: "message_start"; + message: AgentMessage; +} + +/** Fired during assistant message streaming with token-by-token updates */ +export interface MessageUpdateEvent { + type: "message_update"; + message: AgentMessage; + assistantMessageEvent: AssistantMessageEvent; +} + +/** Fired when a message ends */ +export interface MessageEndEvent { + type: "message_end"; + message: AgentMessage; +} + +/** Fired when a tool starts executing */ +export interface ToolExecutionStartEvent { + type: "tool_execution_start"; + toolCallId: string; + toolName: string; + args: unknown; +} + +/** Fired during tool execution with partial/streaming output */ +export interface ToolExecutionUpdateEvent { + type: "tool_execution_update"; + toolCallId: string; + toolName: string; + args: unknown; + partialResult: unknown; +} + +/** Fired when a tool finishes executing */ +export interface ToolExecutionEndEvent { + type: "tool_execution_end"; + toolCallId: string; + toolName: string; + result: unknown; + isError: boolean; +} + +// ============================================================================ +// Model Events +// ============================================================================ + +export type ModelSelectSource = "set" | "cycle" | "restore"; + +/** Fired when a new model is selected */ +export interface ModelSelectEvent { + type: "model_select"; + model: Model; + previousModel: Model | undefined; + source: ModelSelectSource; +} + +/** Fired when a new thinking level is selected */ +export interface ThinkingLevelSelectEvent { + type: "thinking_level_select"; + level: ThinkingLevel; + previousLevel: ThinkingLevel; +} + +// ============================================================================ +// User Bash Events +// ============================================================================ + +/** Fired when user executes a bash command via ! or !! prefix */ +export interface UserBashEvent { + type: "user_bash"; + /** The command to execute */ + command: string; + /** True if !! prefix was used (excluded from LLM context) */ + excludeFromContext: boolean; + /** Current working directory */ + cwd: string; +} + +// ============================================================================ +// Input Events +// ============================================================================ + +/** Source of user input */ +export type InputSource = "interactive" | "rpc" | "extension"; + +/** Fired when user input is received, before agent processing */ +export interface InputEvent { + type: "input"; + /** The input text */ + text: string; + /** Attached images, if any */ + images?: ImageContent[]; + /** Where the input came from */ + source: InputSource; +} + +/** Result from input event handler */ +export type InputEventResult = + | { action: "continue" } + | { action: "transform"; text: string; images?: ImageContent[] } + | { action: "handled" }; + +// ============================================================================ +// Tool Events +// ============================================================================ + +interface ToolCallEventBase { + type: "tool_call"; + toolCallId: string; +} + +export interface BashToolCallEvent extends ToolCallEventBase { + toolName: "bash"; + input: BashToolInput; +} + +export interface ReadToolCallEvent extends ToolCallEventBase { + toolName: "read"; + input: ReadToolInput; +} + +export interface EditToolCallEvent extends ToolCallEventBase { + toolName: "edit"; + input: EditToolInput; +} + +export interface WriteToolCallEvent extends ToolCallEventBase { + toolName: "write"; + input: WriteToolInput; +} + +export interface GrepToolCallEvent extends ToolCallEventBase { + toolName: "grep"; + input: GrepToolInput; +} + +export interface FindToolCallEvent extends ToolCallEventBase { + toolName: "find"; + input: FindToolInput; +} + +export interface LsToolCallEvent extends ToolCallEventBase { + toolName: "ls"; + input: LsToolInput; +} + +export interface CustomToolCallEvent extends ToolCallEventBase { + toolName: string; + input: Record; +} + +/** + * Fired before a tool executes. Can block. + * + * `event.input` is mutable. Mutate it in place to patch tool arguments before execution. + * Later `tool_call` handlers see earlier mutations. No re-validation is performed after mutation. + */ +export type ToolCallEvent = + | BashToolCallEvent + | ReadToolCallEvent + | EditToolCallEvent + | WriteToolCallEvent + | GrepToolCallEvent + | FindToolCallEvent + | LsToolCallEvent + | CustomToolCallEvent; + +interface ToolResultEventBase { + type: "tool_result"; + toolCallId: string; + input: Record; + content: (TextContent | ImageContent)[]; + isError: boolean; +} + +export interface BashToolResultEvent extends ToolResultEventBase { + toolName: "bash"; + details: BashToolDetails | undefined; +} + +export interface ReadToolResultEvent extends ToolResultEventBase { + toolName: "read"; + details: ReadToolDetails | undefined; +} + +export interface EditToolResultEvent extends ToolResultEventBase { + toolName: "edit"; + details: EditToolDetails | undefined; +} + +export interface WriteToolResultEvent extends ToolResultEventBase { + toolName: "write"; + details: undefined; +} + +export interface GrepToolResultEvent extends ToolResultEventBase { + toolName: "grep"; + details: GrepToolDetails | undefined; +} + +export interface FindToolResultEvent extends ToolResultEventBase { + toolName: "find"; + details: FindToolDetails | undefined; +} + +export interface LsToolResultEvent extends ToolResultEventBase { + toolName: "ls"; + details: LsToolDetails | undefined; +} + +export interface CustomToolResultEvent extends ToolResultEventBase { + toolName: string; + details: unknown; +} + +/** Fired after a tool executes. Can modify result. */ +export type ToolResultEvent = + | BashToolResultEvent + | ReadToolResultEvent + | EditToolResultEvent + | WriteToolResultEvent + | GrepToolResultEvent + | FindToolResultEvent + | LsToolResultEvent + | CustomToolResultEvent; + +// Type guards for ToolResultEvent +export function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent { + return e.toolName === "bash"; +} +export function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent { + return e.toolName === "read"; +} +export function isEditToolResult(e: ToolResultEvent): e is EditToolResultEvent { + return e.toolName === "edit"; +} +export function isWriteToolResult(e: ToolResultEvent): e is WriteToolResultEvent { + return e.toolName === "write"; +} +export function isGrepToolResult(e: ToolResultEvent): e is GrepToolResultEvent { + return e.toolName === "grep"; +} +export function isFindToolResult(e: ToolResultEvent): e is FindToolResultEvent { + return e.toolName === "find"; +} +export function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent { + return e.toolName === "ls"; +} + +/** + * Type guard for narrowing ToolCallEvent by tool name. + * + * Built-in tools narrow automatically (no type params needed): + * ```ts + * if (isToolCallEventType("bash", event)) { + * event.input.command; // string + * } + * ``` + * + * Custom tools require explicit type parameters: + * ```ts + * if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) { + * event.input.action; // typed + * } + * ``` + * + * Note: Direct narrowing via `event.toolName === "bash"` doesn't work because + * CustomToolCallEvent.toolName is `string` which overlaps with all literals. + */ +export function isToolCallEventType( + toolName: "bash", + event: ToolCallEvent, +): event is BashToolCallEvent; +export function isToolCallEventType( + toolName: "read", + event: ToolCallEvent, +): event is ReadToolCallEvent; +export function isToolCallEventType( + toolName: "edit", + event: ToolCallEvent, +): event is EditToolCallEvent; +export function isToolCallEventType( + toolName: "write", + event: ToolCallEvent, +): event is WriteToolCallEvent; +export function isToolCallEventType( + toolName: "grep", + event: ToolCallEvent, +): event is GrepToolCallEvent; +export function isToolCallEventType( + toolName: "find", + event: ToolCallEvent, +): event is FindToolCallEvent; +export function isToolCallEventType(toolName: "ls", event: ToolCallEvent): event is LsToolCallEvent; +export function isToolCallEventType>( + toolName: TName, + event: ToolCallEvent, +): event is ToolCallEvent & { toolName: TName; input: TInput }; +export function isToolCallEventType(toolName: string, event: ToolCallEvent): boolean { + return event.toolName === toolName; +} + +/** Union of all event types */ +export type ExtensionEvent = + | ResourcesDiscoverEvent + | SessionEvent + | ContextEvent + | BeforeProviderRequestEvent + | AfterProviderResponseEvent + | BeforeAgentStartEvent + | AgentStartEvent + | AgentEndEvent + | TurnStartEvent + | TurnEndEvent + | MessageStartEvent + | MessageUpdateEvent + | MessageEndEvent + | ToolExecutionStartEvent + | ToolExecutionUpdateEvent + | ToolExecutionEndEvent + | ModelSelectEvent + | ThinkingLevelSelectEvent + | UserBashEvent + | InputEvent + | ToolCallEvent + | ToolResultEvent; + +// ============================================================================ +// Event Results +// ============================================================================ + +export interface ContextEventResult { + messages?: AgentMessage[]; +} + +export type BeforeProviderRequestEventResult = unknown; + +export interface ToolCallEventResult { + /** Block tool execution. To modify arguments, mutate `event.input` in place instead. */ + block?: boolean; + reason?: string; +} + +/** Result from user_bash event handler */ +export interface UserBashEventResult { + /** Custom operations to use for execution */ + operations?: BashOperations; + /** Full replacement: extension handled execution, use this result */ + result?: BashResult; +} + +export interface ToolResultEventResult { + content?: (TextContent | ImageContent)[]; + details?: unknown; + isError?: boolean; +} + +export interface MessageEndEventResult { + /** Replace the finalized message. The replacement must keep the original message role. */ + message?: AgentMessage; +} + +export interface BeforeAgentStartEventResult { + message?: Pick; + /** Replace the system prompt for this turn. If multiple extensions return this, they are chained. */ + systemPrompt?: string; +} + +export interface SessionBeforeSwitchResult { + cancel?: boolean; +} + +export interface SessionBeforeForkResult { + cancel?: boolean; + skipConversationRestore?: boolean; +} + +export interface SessionBeforeCompactResult { + cancel?: boolean; + compaction?: CompactionResult; +} + +export interface SessionBeforeTreeResult { + cancel?: boolean; + summary?: { + summary: string; + details?: unknown; + }; + /** Override custom instructions for summarization */ + customInstructions?: string; + /** Override whether customInstructions replaces the default prompt */ + replaceInstructions?: boolean; + /** Override label to attach to the branch summary entry */ + label?: string; +} + +// ============================================================================ +// Message Rendering +// ============================================================================ + +export interface MessageRenderOptions { + expanded: boolean; +} + +export type MessageRenderer = ( + message: CustomMessage, + options: MessageRenderOptions, + theme: Theme, +) => Component | undefined; + +// ============================================================================ +// Command Registration +// ============================================================================ + +export interface RegisteredCommand { + name: string; + sourceInfo: SourceInfo; + description?: string; + getArgumentCompletions?: ( + argumentPrefix: string, + ) => AutocompleteItem[] | null | Promise; + handler: (args: string, ctx: ExtensionCommandContext) => Promise; +} + +export interface ResolvedCommand extends RegisteredCommand { + invocationName: string; +} + +// ============================================================================ +// Extension API +// ============================================================================ + +/** Handler function type for events */ +// biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements +export type ExtensionHandler = ( + event: E, + ctx: ExtensionContext, +) => Promise | R | void; + +/** + * ExtensionAPI passed to extension factory functions. + */ +export interface ExtensionAPI { + // ========================================================================= + // Event Subscription + // ========================================================================= + + on( + event: "resources_discover", + handler: ExtensionHandler, + ): void; + on(event: "session_start", handler: ExtensionHandler): void; + on( + event: "session_before_switch", + handler: ExtensionHandler, + ): void; + on( + event: "session_before_fork", + handler: ExtensionHandler, + ): void; + on( + event: "session_before_compact", + handler: ExtensionHandler, + ): void; + on(event: "session_compact", handler: ExtensionHandler): void; + on(event: "session_shutdown", handler: ExtensionHandler): void; + on( + event: "session_before_tree", + handler: ExtensionHandler, + ): void; + on(event: "session_tree", handler: ExtensionHandler): void; + on(event: "context", handler: ExtensionHandler): void; + on( + event: "before_provider_request", + handler: ExtensionHandler, + ): void; + on(event: "after_provider_response", handler: ExtensionHandler): void; + on( + event: "before_agent_start", + handler: ExtensionHandler, + ): void; + on(event: "agent_start", handler: ExtensionHandler): void; + on(event: "agent_end", handler: ExtensionHandler): void; + on(event: "turn_start", handler: ExtensionHandler): void; + on(event: "turn_end", handler: ExtensionHandler): void; + on(event: "message_start", handler: ExtensionHandler): void; + on(event: "message_update", handler: ExtensionHandler): void; + on(event: "message_end", handler: ExtensionHandler): void; + on(event: "tool_execution_start", handler: ExtensionHandler): void; + on(event: "tool_execution_update", handler: ExtensionHandler): void; + on(event: "tool_execution_end", handler: ExtensionHandler): void; + on(event: "model_select", handler: ExtensionHandler): void; + on(event: "thinking_level_select", handler: ExtensionHandler): void; + on(event: "tool_call", handler: ExtensionHandler): void; + on(event: "tool_result", handler: ExtensionHandler): void; + on(event: "user_bash", handler: ExtensionHandler): void; + on(event: "input", handler: ExtensionHandler): void; + + // ========================================================================= + // Tool Registration + // ========================================================================= + + /** Register a tool that the LLM can call. */ + registerTool( + tool: ToolDefinition, + ): void; + + // ========================================================================= + // Command, Shortcut, Flag Registration + // ========================================================================= + + /** Register a custom command. */ + registerCommand(name: string, options: Omit): void; + + /** Register a keyboard shortcut. */ + registerShortcut( + shortcut: KeyId, + options: { + description?: string; + handler: (ctx: ExtensionContext) => Promise | void; + }, + ): void; + + /** Register a CLI flag. */ + registerFlag( + name: string, + options: { + description?: string; + type: "boolean" | "string"; + default?: boolean | string; + }, + ): void; + + /** Get the value of a registered CLI flag. */ + getFlag(name: string): boolean | string | undefined; + + // ========================================================================= + // Message Rendering + // ========================================================================= + + /** Register a custom renderer for CustomMessageEntry. */ + registerMessageRenderer(customType: string, renderer: MessageRenderer): void; + + // ========================================================================= + // Actions + // ========================================================================= + + /** Send a custom message to the session. */ + sendMessage( + message: Pick, "customType" | "content" | "display" | "details">, + options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }, + ): void; + + /** + * Send a user message to the agent. Always triggers a turn. + * When the agent is streaming, use deliverAs to specify how to queue the message. + */ + sendUserMessage( + content: string | (TextContent | ImageContent)[], + options?: { deliverAs?: "steer" | "followUp" }, + ): void; + + /** Append a custom entry to the session for state persistence (not sent to LLM). */ + appendEntry(customType: string, data?: unknown): void; + + // ========================================================================= + // Session Metadata + // ========================================================================= + + /** Set the session display name (shown in session selector). */ + setSessionName(name: string): void; + + /** Get the current session name, if set. */ + getSessionName(): string | undefined; + + /** Set or clear a label on an entry. Labels are user-defined markers for bookmarking/navigation. */ + setLabel(entryId: string, label: string | undefined): void; + + /** Execute a shell command. */ + exec(command: string, args: string[], options?: ExecOptions): Promise; + + /** Get the list of currently active tool names. */ + getActiveTools(): string[]; + + /** Get all configured tools with parameter schema and source metadata. */ + getAllTools(): ToolInfo[]; + + /** Set the active tools by name. */ + setActiveTools(toolNames: string[]): void; + + /** Get available slash commands in the current session. */ + getCommands(): SlashCommandInfo[]; + + // ========================================================================= + // Model and Thinking Level + // ========================================================================= + + /** Set the current model. Returns false if no API key available. */ + setModel(model: Model): Promise; + + /** Get current thinking level. */ + getThinkingLevel(): ThinkingLevel; + + /** Set thinking level (clamped to model capabilities). */ + setThinkingLevel(level: ThinkingLevel): void; + + // ========================================================================= + // Provider Registration + // ========================================================================= + + /** + * Register or override a model provider. + * + * If `models` is provided: replaces all existing models for this provider. + * If only `baseUrl` is provided: overrides the URL for existing models. + * If `oauth` is provided: registers OAuth provider for /login support. + * If `streamSimple` is provided: registers a custom API stream handler. + * + * During initial extension load this call is queued and applied once the + * runner has bound its context. After that it takes effect immediately, so + * it is safe to call from command handlers or event callbacks without + * requiring a `/reload`. + * + * @example + * // Register a new provider with custom models + * api.registerProvider("my-proxy", { + * baseUrl: "https://proxy.example.com", + * apiKey: "PROXY_API_KEY", + * api: "anthropic-messages", + * models: [ + * { + * id: "claude-sonnet-4-20250514", + * name: "Claude 4 Sonnet (proxy)", + * reasoning: false, + * input: ["text", "image"], + * cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + * contextWindow: 200000, + * maxTokens: 16384 + * } + * ] + * }); + * + * @example + * // Override baseUrl for an existing provider + * api.registerProvider("anthropic", { + * baseUrl: "https://proxy.example.com" + * }); + * + * @example + * // Register provider with OAuth support + * api.registerProvider("corporate-ai", { + * baseUrl: "https://ai.corp.com", + * api: "openai-responses", + * models: [...], + * oauth: { + * name: "Corporate AI (SSO)", + * async login(callbacks) { ... }, + * async refreshToken(credentials) { ... }, + * getApiKey(credentials) { return credentials.access; } + * } + * }); + */ + registerProvider(name: string, config: ProviderConfig): void; + + /** + * Unregister a previously registered provider. + * + * Removes all models belonging to the named provider and restores unknown + * built-in models that were overridden by it. Has no effect if the provider + * is not currently registered. + * + * Like `registerProvider`, this takes effect immediately when called after + * the initial load phase. + * + * @example + * api.unregisterProvider("my-proxy"); + */ + unregisterProvider(name: string): void; + + /** Shared event bus for extension communication. */ + events: EventBus; +} + +// ============================================================================ +// Provider Registration Types +// ============================================================================ + +/** Configuration for registering a provider via api.registerProvider(). */ +export interface ProviderConfig { + /** Display name for the provider in UI. */ + name?: string; + /** Base URL for the API endpoint. Required when defining models. */ + baseUrl?: string; + /** API key or environment variable name. Required when defining models (unless oauth provided). */ + apiKey?: string; + /** API type. Required at provider or model level when defining models. */ + api?: Api; + /** Optional streamSimple handler for custom APIs. */ + streamSimple?: ( + model: Model, + context: Context, + options?: SimpleStreamOptions, + ) => AssistantMessageEventStream; + /** Custom headers to include in requests. */ + headers?: Record; + /** If true, adds Authorization: Bearer header with the resolved API key. */ + authHeader?: boolean; + /** Models to register. If provided, replaces all existing models for this provider. */ + models?: ProviderModelConfig[]; + /** OAuth provider for /login support. The `id` is set automatically from the provider name. */ + oauth?: { + /** Display name for the provider in login UI. */ + name: string; + /** Run the login flow, return credentials to persist. */ + login(callbacks: OAuthLoginCallbacks): Promise; + /** Refresh expired credentials, return updated credentials to persist. */ + refreshToken(credentials: OAuthCredentials): Promise; + /** Convert credentials to API key string for the provider. */ + getApiKey(credentials: OAuthCredentials): string; + /** Optional: modify models for this provider (e.g., update baseUrl based on credentials). */ + modifyModels?(models: Model[], credentials: OAuthCredentials): Model[]; + }; +} + +/** Configuration for a model within a provider. */ +export interface ProviderModelConfig { + /** Model ID (e.g., "claude-sonnet-4-20250514"). */ + id: string; + /** Display name (e.g., "Claude 4 Sonnet"). */ + name: string; + /** API type override for this model. */ + api?: Api; + /** API endpoint URL override for this model. */ + baseUrl?: string; + /** Whether the model supports extended thinking. */ + reasoning: boolean; + /** Maps OpenClaw thinking levels to provider/model-specific values; null marks a level unsupported. */ + thinkingLevelMap?: Model["thinkingLevelMap"]; + /** Supported input types. */ + input: ("text" | "image")[]; + /** Cost per token (for tracking, can be 0). */ + cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; + /** Maximum context window size in tokens. */ + contextWindow: number; + /** Maximum output tokens. */ + maxTokens: number; + /** Custom headers for this model. */ + headers?: Record; + /** OpenAI compatibility settings. */ + compat?: Model["compat"]; +} + +/** Extension factory function type. Supports both sync and async initialization. */ +export type ExtensionFactory = (api: ExtensionAPI) => void | Promise; + +// ============================================================================ +// Loaded Extension Types +// ============================================================================ + +export interface RegisteredTool { + definition: ToolDefinition; + sourceInfo: SourceInfo; +} + +export interface ExtensionFlag { + name: string; + description?: string; + type: "boolean" | "string"; + default?: boolean | string; + extensionPath: string; +} + +export interface ExtensionShortcut { + shortcut: KeyId; + description?: string; + handler: (ctx: ExtensionContext) => Promise | void; + extensionPath: string; +} + +type HandlerFn = (...args: unknown[]) => Promise; + +export type SendMessageHandler = ( + message: Pick, "customType" | "content" | "display" | "details">, + options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" }, +) => void; + +export type SendUserMessageHandler = ( + content: string | (TextContent | ImageContent)[], + options?: { deliverAs?: "steer" | "followUp" }, +) => void; + +export type AppendEntryHandler = (customType: string, data?: unknown) => void; + +export type SetSessionNameHandler = (name: string) => void; + +export type GetSessionNameHandler = () => string | undefined; + +export type GetActiveToolsHandler = () => string[]; + +/** Tool info with name, description, parameter schema, and source metadata */ +export type ToolInfo = Pick & { + sourceInfo: SourceInfo; +}; + +export type GetAllToolsHandler = () => ToolInfo[]; + +export type GetCommandsHandler = () => SlashCommandInfo[]; + +export type SetActiveToolsHandler = (toolNames: string[]) => void; + +export type RefreshToolsHandler = () => void; + +export type SetModelHandler = (model: Model) => Promise; + +export type GetThinkingLevelHandler = () => ThinkingLevel; + +export type SetThinkingLevelHandler = (level: ThinkingLevel) => void; + +export type SetLabelHandler = (entryId: string, label: string | undefined) => void; + +/** + * Shared state created by loader, used during registration and runtime. + * Contains flag values (defaults set during registration, CLI values set after). + */ +export interface ExtensionRuntimeState { + flagValues: Map; + /** Provider registrations queued during extension loading, processed when runner binds */ + pendingProviderRegistrations: Array<{ + name: string; + config: ProviderConfig; + extensionPath: string; + }>; + /** Throws when this extension instance is stale after runtime replacement. */ + assertActive: () => void; + /** Marks this extension instance as stale after runtime replacement or reload. */ + invalidate: (message?: string) => void; + /** + * Register or unregister a provider. + * + * Before bindCore(): queues registrations / removes from queue. + * After bindCore(): calls ModelRegistry directly for immediate effect. + */ + registerProvider: (name: string, config: ProviderConfig, extensionPath?: string) => void; + unregisterProvider: (name: string, extensionPath?: string) => void; +} + +/** + * Action implementations for ExtensionAPI methods. + * Provided to runner.initialize(), copied into the shared runtime. + */ +export interface ExtensionActions { + sendMessage: SendMessageHandler; + sendUserMessage: SendUserMessageHandler; + appendEntry: AppendEntryHandler; + setSessionName: SetSessionNameHandler; + getSessionName: GetSessionNameHandler; + setLabel: SetLabelHandler; + getActiveTools: GetActiveToolsHandler; + getAllTools: GetAllToolsHandler; + setActiveTools: SetActiveToolsHandler; + refreshTools: RefreshToolsHandler; + getCommands: GetCommandsHandler; + setModel: SetModelHandler; + getThinkingLevel: GetThinkingLevelHandler; + setThinkingLevel: SetThinkingLevelHandler; +} + +/** + * Actions for ExtensionContext (ctx.* in event handlers). + * Required by all modes. + */ +export interface ExtensionContextActions { + getModel: () => Model | undefined; + isIdle: () => boolean; + getSignal: () => AbortSignal | undefined; + abort: () => void; + hasPendingMessages: () => boolean; + shutdown: () => void; + getContextUsage: () => ContextUsage | undefined; + compact: (options?: CompactOptions) => void; + getSystemPrompt: () => string; +} + +/** + * Actions for ExtensionCommandContext (ctx.* in command handlers). + * Only needed for interactive mode where extension commands are invokable. + */ +export interface ExtensionCommandContextActions { + waitForIdle: () => Promise; + newSession: (options?: { + parentSession?: string; + setup?: (sessionManager: SessionManager) => Promise; + withSession?: (ctx: ReplacedSessionContext) => Promise; + }) => Promise<{ cancelled: boolean }>; + fork: ( + entryId: string, + options?: { + position?: "before" | "at"; + withSession?: (ctx: ReplacedSessionContext) => Promise; + }, + ) => Promise<{ cancelled: boolean }>; + navigateTree: ( + targetId: string, + options?: { + summarize?: boolean; + customInstructions?: string; + replaceInstructions?: boolean; + label?: string; + }, + ) => Promise<{ cancelled: boolean }>; + switchSession: ( + sessionPath: string, + options?: { withSession?: (ctx: ReplacedSessionContext) => Promise }, + ) => Promise<{ cancelled: boolean }>; + reload: () => Promise; +} + +/** + * Full runtime = state + actions. + * Created by loader with throwing action stubs, completed by runner.initialize(). + */ +export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionActions {} + +/** Loaded extension with all registered items. */ +export interface Extension { + path: string; + resolvedPath: string; + sourceInfo: SourceInfo; + handlers: Map; + tools: Map; + messageRenderers: Map; + commands: Map; + flags: Map; + shortcuts: Map; +} + +/** Result of loading extensions. */ +export interface LoadExtensionsResult { + extensions: Extension[]; + errors: Array<{ path: string; error: string }>; + /** Shared runtime - actions are throwing stubs until runner.initialize() */ + runtime: ExtensionRuntime; +} + +// ============================================================================ +// Extension Error +// ============================================================================ + +export interface ExtensionError { + extensionPath: string; + event: string; + error: string; + stack?: string; +} diff --git a/src/agents/sessions/extensions/wrapper.ts b/src/agents/sessions/extensions/wrapper.ts new file mode 100644 index 00000000000..f4dc577a2e7 --- /dev/null +++ b/src/agents/sessions/extensions/wrapper.ts @@ -0,0 +1,36 @@ +/** + * Tool wrappers for extension-registered tools. + * + * These wrappers only adapt tool execution so extension tools receive the runner context. + * Tool call and tool result interception is handled by AgentSession via agent-core hooks. + */ + +import type { AgentTool } from "../../runtime/index.js"; +import { wrapToolDefinition, wrapToolDefinitions } from "../tools/tool-definition-wrapper.js"; +import type { ExtensionRunner } from "./runner.js"; +import type { RegisteredTool } from "./types.js"; + +/** + * Wrap a RegisteredTool into an AgentTool. + * Uses the runner's createContext() for consistent context across tools and event handlers. + */ +export function wrapRegisteredTool( + registeredTool: RegisteredTool, + runner: ExtensionRunner, +): AgentTool { + return wrapToolDefinition(registeredTool.definition, () => runner.createContext()); +} + +/** + * Wrap all registered tools into AgentTools. + * Uses the runner's createContext() for consistent context across tools and event handlers. + */ +export function wrapRegisteredTools( + registeredTools: RegisteredTool[], + runner: ExtensionRunner, +): AgentTool[] { + return wrapToolDefinitions( + registeredTools.map((registeredTool) => registeredTool.definition), + () => runner.createContext(), + ); +} diff --git a/src/agents/sessions/footer-data-provider.ts b/src/agents/sessions/footer-data-provider.ts new file mode 100644 index 00000000000..202b4cde473 --- /dev/null +++ b/src/agents/sessions/footer-data-provider.ts @@ -0,0 +1,388 @@ +import { type ExecFileException, execFile, spawnSync } from "node:child_process"; +import { + existsSync, + type FSWatcher, + readFileSync, + statSync, + unwatchFile, + watchFile, +} from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { closeWatcher, FS_WATCH_RETRY_DELAY_MS, watchWithErrorHandler } from "../utils/fs-watch.js"; + +type GitPaths = { + repoDir: string; + commonGitDir: string; + headPath: string; +}; + +/** + * Find git metadata paths by walking up from cwd. + * Handles both regular git repos (.git is a directory) and worktrees (.git is a file). + */ +function findGitPaths(cwd: string): GitPaths | null { + let dir = cwd; + while (true) { + const gitPath = join(dir, ".git"); + if (existsSync(gitPath)) { + try { + const stat = statSync(gitPath); + if (stat.isFile()) { + const content = readFileSync(gitPath, "utf8").trim(); + if (content.startsWith("gitdir: ")) { + const gitDir = resolve(dir, content.slice(8).trim()); + const headPath = join(gitDir, "HEAD"); + if (!existsSync(headPath)) { + return null; + } + const commonDirPath = join(gitDir, "commondir"); + const commonGitDir = existsSync(commonDirPath) + ? resolve(gitDir, readFileSync(commonDirPath, "utf8").trim()) + : gitDir; + return { repoDir: dir, commonGitDir, headPath }; + } + } else if (stat.isDirectory()) { + const headPath = join(gitPath, "HEAD"); + if (!existsSync(headPath)) { + return null; + } + return { repoDir: dir, commonGitDir: gitPath, headPath }; + } + } catch { + return null; + } + } + const parent = dirname(dir); + if (parent === dir) { + return null; + } + dir = parent; + } +} + +/** Ask git for the current branch. Returns null on detached HEAD or if git is unavailable. */ +function resolveBranchWithGitSync(repoDir: string): string | null { + const result = spawnSync( + "git", + ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], + { + cwd: repoDir, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }, + ); + const branch = result.status === 0 ? result.stdout.trim() : ""; + return branch || null; +} + +/** Ask git for the current branch asynchronously. Returns null on detached HEAD or if git is unavailable. */ +function resolveBranchWithGitAsync(repoDir: string): Promise { + return new Promise((resolvePromise) => { + execFile( + "git", + ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], + { + cwd: repoDir, + encoding: "utf8", + }, + (error: ExecFileException | null, stdout: string) => { + if (error) { + resolvePromise(null); + return; + } + const branch = stdout.trim(); + resolvePromise(branch || null); + }, + ); + }); +} + +/** + * Provides git branch and extension statuses - data not otherwise accessible to extensions. + * Token stats, model info available via ctx.sessionManager and ctx.model. + */ +export class FooterDataProvider { + private cwd: string; + private static readonly WATCH_DEBOUNCE_MS = 500; + + private extensionStatuses = new Map(); + private cachedBranch: string | null | undefined = undefined; + private gitPaths: GitPaths | null | undefined = undefined; + private headWatcher: FSWatcher | null = null; + private reftableWatcher: FSWatcher | null = null; + private reftableTablesListWatcher: FSWatcher | null = null; + private reftableTablesListPath: string | null = null; + private branchChangeCallbacks = new Set<() => void>(); + private availableProviderCount = 0; + private refreshTimer: ReturnType | null = null; + private gitWatcherRetryTimer: ReturnType | null = null; + private refreshInFlight = false; + private refreshPending = false; + private disposed = false; + + constructor(cwd: string) { + this.cwd = cwd; + this.gitPaths = findGitPaths(cwd); + this.setupGitWatcher(); + } + + /** Current git branch, null if not in repo, "detached" if detached HEAD */ + getGitBranch(): string | null { + if (this.cachedBranch === undefined) { + this.cachedBranch = this.resolveGitBranchSync(); + } + return this.cachedBranch; + } + + /** Extension status texts set via ctx.ui.setStatus() */ + getExtensionStatuses(): ReadonlyMap { + return this.extensionStatuses; + } + + /** Subscribe to git branch changes. Returns unsubscribe function. */ + onBranchChange(callback: () => void): () => void { + this.branchChangeCallbacks.add(callback); + return () => this.branchChangeCallbacks.delete(callback); + } + + /** Internal: set extension status */ + setExtensionStatus(key: string, text: string | undefined): void { + if (text === undefined) { + this.extensionStatuses.delete(key); + } else { + this.extensionStatuses.set(key, text); + } + } + + /** Internal: clear extension statuses */ + clearExtensionStatuses(): void { + this.extensionStatuses.clear(); + } + + /** Number of unique providers with available models (for footer display) */ + getAvailableProviderCount(): number { + return this.availableProviderCount; + } + + /** Internal: update available provider count */ + setAvailableProviderCount(count: number): void { + this.availableProviderCount = count; + } + + setCwd(cwd: string): void { + if (this.cwd === cwd) { + return; + } + + this.cwd = cwd; + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + this.refreshTimer = null; + } + this.clearGitWatchers(); + this.cachedBranch = undefined; + this.gitPaths = findGitPaths(cwd); + this.setupGitWatcher(); + this.notifyBranchChange(); + } + + /** Internal: cleanup */ + dispose(): void { + this.disposed = true; + if (this.refreshTimer) { + clearTimeout(this.refreshTimer); + this.refreshTimer = null; + } + this.clearGitWatchers(); + this.branchChangeCallbacks.clear(); + } + + private notifyBranchChange(): void { + for (const cb of this.branchChangeCallbacks) { + cb(); + } + } + + private scheduleRefresh(): void { + if (this.disposed || this.refreshTimer) { + return; + } + if (this.refreshInFlight) { + this.refreshPending = true; + return; + } + this.refreshTimer = setTimeout(() => { + this.refreshTimer = null; + void this.refreshGitBranchAsync(); + }, FooterDataProvider.WATCH_DEBOUNCE_MS); + } + + private async refreshGitBranchAsync(): Promise { + if (this.disposed) { + return; + } + if (this.refreshInFlight) { + this.refreshPending = true; + return; + } + + this.refreshInFlight = true; + try { + const nextBranch = await this.resolveGitBranchAsync(); + if (this.disposed) { + return; + } + if (this.cachedBranch !== undefined && this.cachedBranch !== nextBranch) { + this.cachedBranch = nextBranch; + this.notifyBranchChange(); + return; + } + this.cachedBranch = nextBranch; + } finally { + this.refreshInFlight = false; + if (this.refreshPending && !this.disposed) { + this.refreshPending = false; + this.scheduleRefresh(); + } + } + } + + private resolveGitBranchSync(): string | null { + try { + if (!this.gitPaths) { + return null; + } + const content = readFileSync(this.gitPaths.headPath, "utf8").trim(); + if (content.startsWith("ref: refs/heads/")) { + const branch = content.slice(16); + return branch === ".invalid" + ? (resolveBranchWithGitSync(this.gitPaths.repoDir) ?? "detached") + : branch; + } + return "detached"; + } catch { + return null; + } + } + + private async resolveGitBranchAsync(): Promise { + try { + if (!this.gitPaths) { + return null; + } + const content = readFileSync(this.gitPaths.headPath, "utf8").trim(); + if (content.startsWith("ref: refs/heads/")) { + const branch = content.slice(16); + return branch === ".invalid" + ? ((await resolveBranchWithGitAsync(this.gitPaths.repoDir)) ?? "detached") + : branch; + } + return "detached"; + } catch { + return null; + } + } + + private clearGitWatchers(): void { + closeWatcher(this.headWatcher); + this.headWatcher = null; + closeWatcher(this.reftableWatcher); + this.reftableWatcher = null; + closeWatcher(this.reftableTablesListWatcher); + this.reftableTablesListWatcher = null; + if (this.reftableTablesListPath) { + unwatchFile(this.reftableTablesListPath); + this.reftableTablesListPath = null; + } + if (this.gitWatcherRetryTimer) { + clearTimeout(this.gitWatcherRetryTimer); + this.gitWatcherRetryTimer = null; + } + } + + private scheduleGitWatcherRetry(): void { + if (this.disposed || this.gitWatcherRetryTimer) { + return; + } + + this.gitWatcherRetryTimer = setTimeout(() => { + this.gitWatcherRetryTimer = null; + this.setupGitWatcher(); + }, FS_WATCH_RETRY_DELAY_MS); + } + + private handleGitWatcherError(): void { + this.clearGitWatchers(); + this.scheduleGitWatcherRetry(); + } + + private setupGitWatcher(): void { + this.clearGitWatchers(); + if (!this.gitPaths) { + return; + } + + // Watch the directory containing HEAD, not HEAD itself. + // Git uses atomic writes (write temp, rename over HEAD), which changes the inode. + // fs.watch on a file stops working after the inode changes. + this.headWatcher = watchWithErrorHandler( + dirname(this.gitPaths.headPath), + (eventType, filename) => { + void eventType; + if (!filename || filename === "HEAD") { + this.scheduleRefresh(); + } + }, + () => this.handleGitWatcherError(), + ); + if (!this.headWatcher) { + return; + } + + // In reftable repos, branch switches update files in the reftable directory + // instead of HEAD. Watch it separately so the footer picks up those changes. + const reftableDir = join(this.gitPaths.commonGitDir, "reftable"); + if (existsSync(reftableDir)) { + this.reftableWatcher = watchWithErrorHandler( + reftableDir, + () => { + this.scheduleRefresh(); + }, + () => this.handleGitWatcherError(), + ); + if (!this.reftableWatcher) { + return; + } + + const tablesListPath = join(reftableDir, "tables.list"); + if (existsSync(tablesListPath)) { + this.reftableTablesListPath = tablesListPath; + this.reftableTablesListWatcher = watchWithErrorHandler( + tablesListPath, + () => { + this.scheduleRefresh(); + }, + () => this.handleGitWatcherError(), + ); + if (!this.reftableTablesListWatcher) { + return; + } + watchFile(tablesListPath, { interval: 250 }, (current, previous) => { + if ( + current.mtimeMs !== previous.mtimeMs || + current.ctimeMs !== previous.ctimeMs || + current.size !== previous.size + ) { + this.scheduleRefresh(); + } + }); + } + } + } +} + +/** Read-only view for extensions - excludes setExtensionStatus, setAvailableProviderCount and dispose */ +export type ReadonlyFooterDataProvider = Pick< + FooterDataProvider, + "getGitBranch" | "getExtensionStatuses" | "getAvailableProviderCount" | "onBranchChange" +>; diff --git a/src/agents/sessions/http-dispatcher.ts b/src/agents/sessions/http-dispatcher.ts new file mode 100644 index 00000000000..36ef0878d76 --- /dev/null +++ b/src/agents/sessions/http-dispatcher.ts @@ -0,0 +1,55 @@ +import * as undici from "undici"; + +export const DEFAULT_HTTP_IDLE_TIMEOUT_MS = 300_000; + +export const HTTP_IDLE_TIMEOUT_CHOICES = [ + { label: "30 sec", timeoutMs: 30_000 }, + { label: "1 min", timeoutMs: 60_000 }, + { label: "2 min", timeoutMs: 120_000 }, + { label: "5 min", timeoutMs: 300_000 }, + { label: "disabled", timeoutMs: 0 }, +] as const; + +export function parseHttpIdleTimeoutMs(value: unknown): number | undefined { + if (typeof value === "string") { + const trimmed = value.trim(); + if (trimmed.toLowerCase() === "disabled") { + return 0; + } + if (trimmed.length === 0) { + return undefined; + } + return parseHttpIdleTimeoutMs(Number(trimmed)); + } + + if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { + return undefined; + } + return Math.floor(value); +} + +export function formatHttpIdleTimeoutMs(timeoutMs: number): string { + const choice = HTTP_IDLE_TIMEOUT_CHOICES.find((item) => item.timeoutMs === timeoutMs); + if (choice) { + return choice.label; + } + return `${timeoutMs / 1000} sec`; +} + +export function configureHttpDispatcher(timeoutMs: number = DEFAULT_HTTP_IDLE_TIMEOUT_MS): void { + const normalizedTimeoutMs = parseHttpIdleTimeoutMs(timeoutMs); + if (normalizedTimeoutMs === undefined) { + throw new Error(`Invalid HTTP idle timeout: ${String(timeoutMs)}`); + } + undici.setGlobalDispatcher( + new undici.EnvHttpProxyAgent({ + allowH2: false, + bodyTimeout: normalizedTimeoutMs, + headersTimeout: normalizedTimeoutMs, + }), + ); + // Keep fetch and the dispatcher on the same undici implementation. Node 26.0's + // bundled fetch can otherwise consume compressed responses through npm undici's + // dispatcher without decompressing them, causing response.json() failures. + undici.install?.(); +} diff --git a/src/agents/sessions/index.ts b/src/agents/sessions/index.ts new file mode 100644 index 00000000000..ee30f7a3382 --- /dev/null +++ b/src/agents/sessions/index.ts @@ -0,0 +1,43 @@ +/** + * OpenClaw-owned agent session runtime. + */ + +export { getAgentDir, VERSION } from "../config.js"; +export * from "./agent-session.js"; +export * from "./agent-session-runtime.js"; +export * from "./agent-session-services.js"; +export * from "./auth-storage.js"; +export * from "./bash-executor.js"; +export * from "./compaction/index.js"; +export * from "./event-bus.js"; +export * from "./extensions/index.js"; +export type { ReadonlyFooterDataProvider } from "./footer-data-provider.js"; +export { convertToLlm } from "./messages.js"; +export * from "./model-registry.js"; +export * from "./model-resolver.js"; +export * from "./package-manager.js"; +export * from "./resource-loader.js"; +export * from "./sdk.js"; +export * from "./session-manager.js"; +export { + FileSettingsStorage, + InMemorySettingsStorage, + SettingsManager, + type BranchSummarySettings, + type ImageSettings, + type MarkdownSettings, + type PackageSource, + type ProviderRetrySettings, + type RetrySettings, + type Settings, + type SettingsError, + type SettingsScope, + type SettingsStorage, + type TerminalSettings, + type ThinkingBudgetsSettings, + type TransportSetting, + type WarningSettings, +} from "./settings-manager.js"; +export * from "./skills.js"; +export * from "./source-info.js"; +export * from "./tools/index.js"; diff --git a/src/agents/sessions/keybindings.ts b/src/agents/sessions/keybindings.ts new file mode 100644 index 00000000000..b6e65589df4 --- /dev/null +++ b/src/agents/sessions/keybindings.ts @@ -0,0 +1,378 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { + type Keybinding, + type KeybindingDefinitions, + type KeybindingsConfig, + type KeyId, + TUI_KEYBINDINGS, + KeybindingsManager as TuiKeybindingsManager, +} from "@earendil-works/pi-tui"; +import { getAgentDir } from "../config.js"; + +export interface AppKeybindings { + "app.interrupt": true; + "app.clear": true; + "app.exit": true; + "app.suspend": true; + "app.thinking.cycle": true; + "app.model.cycleForward": true; + "app.model.cycleBackward": true; + "app.model.select": true; + "app.tools.expand": true; + "app.thinking.toggle": true; + "app.session.toggleNamedFilter": true; + "app.editor.external": true; + "app.message.followUp": true; + "app.message.dequeue": true; + "app.clipboard.pasteImage": true; + "app.session.new": true; + "app.session.tree": true; + "app.session.fork": true; + "app.session.resume": true; + "app.tree.foldOrUp": true; + "app.tree.unfoldOrDown": true; + "app.tree.editLabel": true; + "app.tree.toggleLabelTimestamp": true; + "app.session.togglePath": true; + "app.session.toggleSort": true; + "app.session.rename": true; + "app.session.delete": true; + "app.session.deleteNoninvasive": true; + "app.models.save": true; + "app.models.enableAll": true; + "app.models.clearAll": true; + "app.models.toggleProvider": true; + "app.models.reorderUp": true; + "app.models.reorderDown": true; + "app.tree.filter.default": true; + "app.tree.filter.noTools": true; + "app.tree.filter.userOnly": true; + "app.tree.filter.labeledOnly": true; + "app.tree.filter.all": true; + "app.tree.filter.cycleForward": true; + "app.tree.filter.cycleBackward": true; +} + +export type AppKeybinding = keyof AppKeybindings; + +declare module "@earendil-works/pi-tui" { + interface Keybindings extends AppKeybindings {} +} + +export const KEYBINDINGS = { + ...TUI_KEYBINDINGS, + "app.interrupt": { defaultKeys: "escape", description: "Cancel or abort" }, + "app.clear": { defaultKeys: "ctrl+c", description: "Clear editor" }, + "app.exit": { defaultKeys: "ctrl+d", description: "Exit when editor is empty" }, + "app.suspend": { + defaultKeys: process.platform === "win32" ? [] : "ctrl+z", + description: "Suspend to background", + }, + "app.thinking.cycle": { + defaultKeys: "shift+tab", + description: "Cycle thinking level", + }, + "app.model.cycleForward": { + defaultKeys: "ctrl+p", + description: "Cycle to next model", + }, + "app.model.cycleBackward": { + defaultKeys: "shift+ctrl+p", + description: "Cycle to previous model", + }, + "app.model.select": { defaultKeys: "ctrl+l", description: "Open model selector" }, + "app.tools.expand": { defaultKeys: "ctrl+o", description: "Toggle tool output" }, + "app.thinking.toggle": { + defaultKeys: "ctrl+t", + description: "Toggle thinking blocks", + }, + "app.session.toggleNamedFilter": { + defaultKeys: "ctrl+n", + description: "Toggle named session filter", + }, + "app.editor.external": { + defaultKeys: "ctrl+g", + description: "Open external editor", + }, + "app.message.followUp": { + defaultKeys: "alt+enter", + description: "Queue follow-up message", + }, + "app.message.dequeue": { + defaultKeys: "alt+up", + description: "Restore queued messages", + }, + "app.clipboard.pasteImage": { + defaultKeys: process.platform === "win32" ? "alt+v" : "ctrl+v", + description: "Paste image from clipboard", + }, + "app.session.new": { defaultKeys: [], description: "Start a new session" }, + "app.session.tree": { defaultKeys: [], description: "Open session tree" }, + "app.session.fork": { defaultKeys: [], description: "Fork current session" }, + "app.session.resume": { defaultKeys: [], description: "Resume a session" }, + "app.tree.foldOrUp": { + defaultKeys: ["ctrl+left", "alt+left"], + description: "Fold tree branch or move up", + }, + "app.tree.unfoldOrDown": { + defaultKeys: ["ctrl+right", "alt+right"], + description: "Unfold tree branch or move down", + }, + "app.tree.editLabel": { + defaultKeys: "shift+l", + description: "Edit tree label", + }, + "app.tree.toggleLabelTimestamp": { + defaultKeys: "shift+t", + description: "Toggle tree label timestamps", + }, + "app.session.togglePath": { + defaultKeys: "ctrl+p", + description: "Toggle session path display", + }, + "app.session.toggleSort": { + defaultKeys: "ctrl+s", + description: "Toggle session sort mode", + }, + "app.session.rename": { + defaultKeys: "ctrl+r", + description: "Rename session", + }, + "app.session.delete": { + defaultKeys: "ctrl+d", + description: "Delete session", + }, + "app.session.deleteNoninvasive": { + defaultKeys: "ctrl+backspace", + description: "Delete session when query is empty", + }, + "app.models.save": { + defaultKeys: "ctrl+s", + description: "Save model selection", + }, + "app.models.enableAll": { + defaultKeys: "ctrl+a", + description: "Enable all models", + }, + "app.models.clearAll": { + defaultKeys: "ctrl+x", + description: "Clear all models", + }, + "app.models.toggleProvider": { + defaultKeys: "ctrl+p", + description: "Toggle all models for provider", + }, + "app.models.reorderUp": { + defaultKeys: "alt+up", + description: "Move model up in order", + }, + "app.models.reorderDown": { + defaultKeys: "alt+down", + description: "Move model down in order", + }, + "app.tree.filter.default": { + defaultKeys: "ctrl+d", + description: "Tree filter: default view", + }, + "app.tree.filter.noTools": { + defaultKeys: "ctrl+t", + description: "Tree filter: hide tool results", + }, + "app.tree.filter.userOnly": { + defaultKeys: "ctrl+u", + description: "Tree filter: user messages only", + }, + "app.tree.filter.labeledOnly": { + defaultKeys: "ctrl+l", + description: "Tree filter: labeled entries only", + }, + "app.tree.filter.all": { + defaultKeys: "ctrl+a", + description: "Tree filter: show all entries", + }, + "app.tree.filter.cycleForward": { + defaultKeys: "ctrl+o", + description: "Tree filter: cycle forward", + }, + "app.tree.filter.cycleBackward": { + defaultKeys: "shift+ctrl+o", + description: "Tree filter: cycle backward", + }, +} as const satisfies KeybindingDefinitions; + +const KEYBINDING_NAME_MIGRATIONS = { + cursorUp: "tui.editor.cursorUp", + cursorDown: "tui.editor.cursorDown", + cursorLeft: "tui.editor.cursorLeft", + cursorRight: "tui.editor.cursorRight", + cursorWordLeft: "tui.editor.cursorWordLeft", + cursorWordRight: "tui.editor.cursorWordRight", + cursorLineStart: "tui.editor.cursorLineStart", + cursorLineEnd: "tui.editor.cursorLineEnd", + jumpForward: "tui.editor.jumpForward", + jumpBackward: "tui.editor.jumpBackward", + pageUp: "tui.editor.pageUp", + pageDown: "tui.editor.pageDown", + deleteCharBackward: "tui.editor.deleteCharBackward", + deleteCharForward: "tui.editor.deleteCharForward", + deleteWordBackward: "tui.editor.deleteWordBackward", + deleteWordForward: "tui.editor.deleteWordForward", + deleteToLineStart: "tui.editor.deleteToLineStart", + deleteToLineEnd: "tui.editor.deleteToLineEnd", + yank: "tui.editor.yank", + yankPop: "tui.editor.yankPop", + undo: "tui.editor.undo", + newLine: "tui.input.newLine", + submit: "tui.input.submit", + tab: "tui.input.tab", + copy: "tui.input.copy", + selectUp: "tui.select.up", + selectDown: "tui.select.down", + selectPageUp: "tui.select.pageUp", + selectPageDown: "tui.select.pageDown", + selectConfirm: "tui.select.confirm", + selectCancel: "tui.select.cancel", + interrupt: "app.interrupt", + clear: "app.clear", + exit: "app.exit", + suspend: "app.suspend", + cycleThinkingLevel: "app.thinking.cycle", + cycleModelForward: "app.model.cycleForward", + cycleModelBackward: "app.model.cycleBackward", + selectModel: "app.model.select", + expandTools: "app.tools.expand", + toggleThinking: "app.thinking.toggle", + toggleSessionNamedFilter: "app.session.toggleNamedFilter", + externalEditor: "app.editor.external", + followUp: "app.message.followUp", + dequeue: "app.message.dequeue", + pasteImage: "app.clipboard.pasteImage", + newSession: "app.session.new", + tree: "app.session.tree", + fork: "app.session.fork", + resume: "app.session.resume", + treeFoldOrUp: "app.tree.foldOrUp", + treeUnfoldOrDown: "app.tree.unfoldOrDown", + treeEditLabel: "app.tree.editLabel", + treeToggleLabelTimestamp: "app.tree.toggleLabelTimestamp", + toggleSessionPath: "app.session.togglePath", + toggleSessionSort: "app.session.toggleSort", + renameSession: "app.session.rename", + deleteSession: "app.session.delete", + deleteSessionNoninvasive: "app.session.deleteNoninvasive", +} as const satisfies Record; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAME_MIGRATIONS { + return key in KEYBINDING_NAME_MIGRATIONS; +} + +function toKeybindingsConfig(value: unknown): KeybindingsConfig { + if (!isRecord(value)) { + return {}; + } + + const config: KeybindingsConfig = {}; + for (const [key, binding] of Object.entries(value)) { + if (typeof binding === "string") { + config[key] = binding as KeyId; + continue; + } + if (Array.isArray(binding) && binding.every((entry) => typeof entry === "string")) { + config[key] = binding as KeyId[]; + } + } + return config; +} + +export function migrateKeybindingsConfig(rawConfig: Record): { + config: Record; + migrated: boolean; +} { + const config: Record = {}; + let migrated = false; + + for (const [key, value] of Object.entries(rawConfig)) { + const nextKey = isLegacyKeybindingName(key) ? KEYBINDING_NAME_MIGRATIONS[key] : key; + if (nextKey !== key) { + migrated = true; + } + if (key !== nextKey && Object.hasOwn(rawConfig, nextKey)) { + migrated = true; + continue; + } + config[nextKey] = value; + } + + return { config: orderKeybindingsConfig(config), migrated }; +} + +function orderKeybindingsConfig(config: Record): Record { + const ordered: Record = {}; + for (const keybinding of Object.keys(KEYBINDINGS)) { + if (Object.hasOwn(config, keybinding)) { + ordered[keybinding] = config[keybinding]; + } + } + + const extras = Object.keys(config) + .filter((key) => !Object.hasOwn(ordered, key)) + .toSorted(); + for (const key of extras) { + ordered[key] = config[key]; + } + + return ordered; +} + +function loadRawConfig(path: string): Record | undefined { + if (!existsSync(path)) { + return undefined; + } + try { + const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown; + return isRecord(parsed) ? parsed : undefined; + } catch { + return undefined; + } +} + +export class KeybindingsManager extends TuiKeybindingsManager { + private configPath: string | undefined; + + constructor(userBindings: KeybindingsConfig = {}, configPath?: string) { + super(KEYBINDINGS, userBindings); + this.configPath = configPath; + } + + static create(agentDir: string = getAgentDir()): KeybindingsManager { + const configPath = join(agentDir, "keybindings.json"); + const userBindings = KeybindingsManager.loadFromFile(configPath); + return new KeybindingsManager(userBindings, configPath); + } + + reload(): void { + if (!this.configPath) { + return; + } + this.setUserBindings(KeybindingsManager.loadFromFile(this.configPath)); + } + + getEffectiveConfig(): KeybindingsConfig { + return this.getResolvedBindings(); + } + + private static loadFromFile(path: string): KeybindingsConfig { + const rawConfig = loadRawConfig(path); + if (!rawConfig) { + return {}; + } + return toKeybindingsConfig(migrateKeybindingsConfig(rawConfig).config); + } +} + +export type { Keybinding, KeyId, KeybindingsConfig }; diff --git a/src/agents/sessions/messages.ts b/src/agents/sessions/messages.ts new file mode 100644 index 00000000000..53045925f0b --- /dev/null +++ b/src/agents/sessions/messages.ts @@ -0,0 +1,210 @@ +/** + * Custom message types and transformers for the coding agent. + * + * Extends the base AgentMessage type with coding-agent specific message types, + * and provides a transformer to convert them to LLM-compatible messages. + */ + +import type { ImageContent, Message, TextContent } from "openclaw/plugin-sdk/llm"; +import type { AgentMessage } from "../runtime/index.js"; + +export const COMPACTION_SUMMARY_PREFIX = `The conversation history before this point was compacted into the following summary: + + +`; + +export const COMPACTION_SUMMARY_SUFFIX = ` +`; + +export const BRANCH_SUMMARY_PREFIX = `The following is a summary of a branch that this conversation came back from: + + +`; + +export const BRANCH_SUMMARY_SUFFIX = ``; + +/** + * Message type for bash executions via the ! command. + */ +export interface BashExecutionMessage { + role: "bashExecution"; + command: string; + output: string; + exitCode: number | undefined; + cancelled: boolean; + truncated: boolean; + fullOutputPath?: string; + timestamp: number; + /** If true, this message is excluded from LLM context (!! prefix) */ + excludeFromContext?: boolean; +} + +/** + * Message type for extension-injected messages via sendMessage(). + * These are custom messages that extensions can inject into the conversation. + */ +export interface CustomMessage { + role: "custom"; + customType: string; + content: string | (TextContent | ImageContent)[]; + display: boolean; + details?: T; + timestamp: number; +} + +export interface BranchSummaryMessage { + role: "branchSummary"; + summary: string; + fromId: string; + timestamp: number; +} + +export interface CompactionSummaryMessage { + role: "compactionSummary"; + summary: string; + tokensBefore: number; + timestamp: number; +} + +// Extend CustomAgentMessages via declaration merging +declare module "openclaw/plugin-sdk/agent-core" { + interface CustomAgentMessages { + bashExecution: BashExecutionMessage; + custom: CustomMessage; + branchSummary: BranchSummaryMessage; + compactionSummary: CompactionSummaryMessage; + } +} + +/** + * Convert a BashExecutionMessage to user message text for LLM context. + */ +export function bashExecutionToText(msg: BashExecutionMessage): string { + let text = `Ran \`${msg.command}\`\n`; + if (msg.output) { + text += `\`\`\`\n${msg.output}\n\`\`\``; + } else { + text += "(no output)"; + } + if (msg.cancelled) { + text += "\n\n(command cancelled)"; + } else if (msg.exitCode !== null && msg.exitCode !== undefined && msg.exitCode !== 0) { + text += `\n\nCommand exited with code ${msg.exitCode}`; + } + if (msg.truncated && msg.fullOutputPath) { + text += `\n\n[Output truncated. Full output: ${msg.fullOutputPath}]`; + } + return text; +} + +export function createBranchSummaryMessage( + summary: string, + fromId: string, + timestamp: string, +): BranchSummaryMessage { + return { + role: "branchSummary", + summary, + fromId, + timestamp: new Date(timestamp).getTime(), + }; +} + +export function createCompactionSummaryMessage( + summary: string, + tokensBefore: number, + timestamp: string, +): CompactionSummaryMessage { + return { + role: "compactionSummary", + summary: summary, + tokensBefore, + timestamp: new Date(timestamp).getTime(), + }; +} + +/** Convert CustomMessageEntry to AgentMessage format */ +export function createCustomMessage( + customType: string, + content: string | (TextContent | ImageContent)[], + display: boolean, + details: unknown, + timestamp: string, +): CustomMessage { + return { + role: "custom", + customType, + content, + display, + details, + timestamp: new Date(timestamp).getTime(), + }; +} + +/** + * Transform AgentMessages (including custom types) to LLM-compatible Messages. + * + * This is used by: + * - Agent's transormToLlm option (for prompt calls and queued messages) + * - Compaction's generateSummary (for summarization) + * - Custom extensions and tools + */ +export function convertToLlm(messages: AgentMessage[]): Message[] { + return messages + .map((m): Message | undefined => { + switch (m.role) { + case "bashExecution": + // Skip messages excluded from context (!! prefix) + if (m.excludeFromContext) { + return undefined; + } + return { + role: "user", + content: [{ type: "text", text: bashExecutionToText(m) }], + timestamp: m.timestamp, + }; + case "custom": { + const content = + typeof m.content === "string" + ? [{ type: "text" as const, text: m.content }] + : m.content; + return { + role: "user", + content, + timestamp: m.timestamp, + }; + } + case "branchSummary": + return { + role: "user", + content: [ + { + type: "text" as const, + text: BRANCH_SUMMARY_PREFIX + m.summary + BRANCH_SUMMARY_SUFFIX, + }, + ], + timestamp: m.timestamp, + }; + case "compactionSummary": + return { + role: "user", + content: [ + { + type: "text" as const, + text: COMPACTION_SUMMARY_PREFIX + m.summary + COMPACTION_SUMMARY_SUFFIX, + }, + ], + timestamp: m.timestamp, + }; + case "user": + case "assistant": + case "toolResult": + return m; + default: + // biome-ignore lint/correctness/noSwitchDeclarations: fine + m satisfies never; + return undefined; + } + }) + .filter((m) => m !== undefined); +} diff --git a/src/agents/sessions/model-registry.ts b/src/agents/sessions/model-registry.ts new file mode 100644 index 00000000000..53029c0b353 --- /dev/null +++ b/src/agents/sessions/model-registry.ts @@ -0,0 +1,1036 @@ +/** + * Model registry - manages built-in and custom models, provides API key resolution. + */ + +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { + type AnthropicMessagesCompat, + type Api, + type AssistantMessageEventStream, + type Context, + getModels, + getProviders, + type KnownProvider, + type Model, + type OAuthProviderInterface, + type OpenAICompletionsCompat, + type OpenAIResponsesCompat, + registerApiProvider, + resetApiProviders, + type SimpleStreamOptions, +} from "openclaw/plugin-sdk/llm"; +import { registerOAuthProvider, resetOAuthProviders } from "openclaw/plugin-sdk/llm-oauth"; +import { type Static, Type } from "typebox"; +import { Compile } from "typebox/compile"; +import type { TLocalizedValidationError } from "typebox/error"; +import { getAgentDir } from "../config.js"; +import type { AuthStatus, AuthStorage } from "./auth-storage.js"; +import { BUILT_IN_PROVIDER_DISPLAY_NAMES } from "./provider-display-names.js"; +import { + clearConfigValueCache, + resolveConfigValueOrThrow, + resolveConfigValueUncached, + resolveHeadersOrThrow, +} from "./resolve-config-value.js"; + +// Schema for OpenRouter routing preferences +const PercentileCutoffsSchema = Type.Object({ + p50: Type.Optional(Type.Number()), + p75: Type.Optional(Type.Number()), + p90: Type.Optional(Type.Number()), + p99: Type.Optional(Type.Number()), +}); + +const OpenRouterRoutingSchema = Type.Object({ + allow_fallbacks: Type.Optional(Type.Boolean()), + require_parameters: Type.Optional(Type.Boolean()), + data_collection: Type.Optional(Type.Union([Type.Literal("deny"), Type.Literal("allow")])), + zdr: Type.Optional(Type.Boolean()), + enforce_distillable_text: Type.Optional(Type.Boolean()), + order: Type.Optional(Type.Array(Type.String())), + only: Type.Optional(Type.Array(Type.String())), + ignore: Type.Optional(Type.Array(Type.String())), + quantizations: Type.Optional(Type.Array(Type.String())), + sort: Type.Optional( + Type.Union([ + Type.String(), + Type.Object({ + by: Type.Optional(Type.String()), + partition: Type.Optional(Type.Union([Type.String(), Type.Null()])), + }), + ]), + ), + max_price: Type.Optional( + Type.Object({ + prompt: Type.Optional(Type.Union([Type.Number(), Type.String()])), + completion: Type.Optional(Type.Union([Type.Number(), Type.String()])), + image: Type.Optional(Type.Union([Type.Number(), Type.String()])), + audio: Type.Optional(Type.Union([Type.Number(), Type.String()])), + request: Type.Optional(Type.Union([Type.Number(), Type.String()])), + }), + ), + preferred_min_throughput: Type.Optional(Type.Union([Type.Number(), PercentileCutoffsSchema])), + preferred_max_latency: Type.Optional(Type.Union([Type.Number(), PercentileCutoffsSchema])), +}); + +// Schema for Vercel AI Gateway routing preferences +const VercelGatewayRoutingSchema = Type.Object({ + only: Type.Optional(Type.Array(Type.String())), + order: Type.Optional(Type.Array(Type.String())), +}); + +// Schema for thinking level support and provider-specific values +const ThinkingLevelMapValueSchema = Type.Union([Type.String(), Type.Null()]); +const ThinkingLevelMapSchema = Type.Object({ + off: Type.Optional(ThinkingLevelMapValueSchema), + minimal: Type.Optional(ThinkingLevelMapValueSchema), + low: Type.Optional(ThinkingLevelMapValueSchema), + medium: Type.Optional(ThinkingLevelMapValueSchema), + high: Type.Optional(ThinkingLevelMapValueSchema), + xhigh: Type.Optional(ThinkingLevelMapValueSchema), +}); + +const OpenAICompletionsCompatSchema = Type.Object({ + supportsStore: Type.Optional(Type.Boolean()), + supportsDeveloperRole: Type.Optional(Type.Boolean()), + supportsReasoningEffort: Type.Optional(Type.Boolean()), + supportsUsageInStreaming: Type.Optional(Type.Boolean()), + maxTokensField: Type.Optional( + Type.Union([Type.Literal("max_completion_tokens"), Type.Literal("max_tokens")]), + ), + requiresToolResultName: Type.Optional(Type.Boolean()), + requiresAssistantAfterToolResult: Type.Optional(Type.Boolean()), + requiresThinkingAsText: Type.Optional(Type.Boolean()), + requiresReasoningContentOnAssistantMessages: Type.Optional(Type.Boolean()), + thinkingFormat: Type.Optional( + Type.Union([ + Type.Literal("openai"), + Type.Literal("openrouter"), + Type.Literal("together"), + Type.Literal("deepseek"), + Type.Literal("zai"), + Type.Literal("qwen"), + Type.Literal("qwen-chat-template"), + ]), + ), + cacheControlFormat: Type.Optional(Type.Literal("anthropic")), + openRouterRouting: Type.Optional(OpenRouterRoutingSchema), + vercelGatewayRouting: Type.Optional(VercelGatewayRoutingSchema), + supportsStrictMode: Type.Optional(Type.Boolean()), + supportsLongCacheRetention: Type.Optional(Type.Boolean()), +}); + +const OpenAIResponsesCompatSchema = Type.Object({ + sendSessionIdHeader: Type.Optional(Type.Boolean()), + supportsLongCacheRetention: Type.Optional(Type.Boolean()), +}); + +const AnthropicMessagesCompatSchema = Type.Object({ + supportsEagerToolInputStreaming: Type.Optional(Type.Boolean()), + supportsLongCacheRetention: Type.Optional(Type.Boolean()), +}); + +const ProviderCompatSchema = Type.Union([ + OpenAICompletionsCompatSchema, + OpenAIResponsesCompatSchema, + AnthropicMessagesCompatSchema, +]); + +// Schema for custom model definition +// Most fields are optional with sensible defaults for local models (Ollama, LM Studio, etc.) +const ModelDefinitionSchema = Type.Object({ + id: Type.String({ minLength: 1 }), + name: Type.Optional(Type.String({ minLength: 1 })), + api: Type.Optional(Type.String({ minLength: 1 })), + baseUrl: Type.Optional(Type.String({ minLength: 1 })), + reasoning: Type.Optional(Type.Boolean()), + thinkingLevelMap: Type.Optional(ThinkingLevelMapSchema), + input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))), + cost: Type.Optional( + Type.Object({ + input: Type.Number(), + output: Type.Number(), + cacheRead: Type.Number(), + cacheWrite: Type.Number(), + }), + ), + contextWindow: Type.Optional(Type.Number()), + maxTokens: Type.Optional(Type.Number()), + headers: Type.Optional(Type.Record(Type.String(), Type.String())), + compat: Type.Optional(ProviderCompatSchema), +}); + +// Schema for per-model overrides (all fields optional, merged with built-in model) +const ModelOverrideSchema = Type.Object({ + name: Type.Optional(Type.String({ minLength: 1 })), + reasoning: Type.Optional(Type.Boolean()), + thinkingLevelMap: Type.Optional(ThinkingLevelMapSchema), + input: Type.Optional(Type.Array(Type.Union([Type.Literal("text"), Type.Literal("image")]))), + cost: Type.Optional( + Type.Object({ + input: Type.Optional(Type.Number()), + output: Type.Optional(Type.Number()), + cacheRead: Type.Optional(Type.Number()), + cacheWrite: Type.Optional(Type.Number()), + }), + ), + contextWindow: Type.Optional(Type.Number()), + maxTokens: Type.Optional(Type.Number()), + headers: Type.Optional(Type.Record(Type.String(), Type.String())), + compat: Type.Optional(ProviderCompatSchema), +}); + +type ModelOverride = Static; + +const ProviderConfigSchema = Type.Object({ + name: Type.Optional(Type.String({ minLength: 1 })), + baseUrl: Type.Optional(Type.String({ minLength: 1 })), + apiKey: Type.Optional(Type.String({ minLength: 1 })), + api: Type.Optional(Type.String({ minLength: 1 })), + headers: Type.Optional(Type.Record(Type.String(), Type.String())), + compat: Type.Optional(ProviderCompatSchema), + authHeader: Type.Optional(Type.Boolean()), + models: Type.Optional(Type.Array(ModelDefinitionSchema)), + modelOverrides: Type.Optional(Type.Record(Type.String(), ModelOverrideSchema)), +}); + +const ModelsConfigSchema = Type.Object({ + providers: Type.Record(Type.String(), ProviderConfigSchema), +}); + +const validateModelsConfig = Compile(ModelsConfigSchema); + +type ModelsConfig = Static; + +function formatValidationPath(error: TLocalizedValidationError): string { + if (error.keyword === "required") { + const requiredProperties = (error.params as { requiredProperties?: string[] }) + .requiredProperties; + const requiredProperty = requiredProperties?.[0]; + if (requiredProperty) { + const basePath = error.instancePath.replace(/^\//, "").replace(/\//g, "."); + return basePath ? `${basePath}.${requiredProperty}` : requiredProperty; + } + } + const path = error.instancePath.replace(/^\//, "").replace(/\//g, "."); + return path || "root"; +} + +/** Strip `//` line comments and trailing commas from JSON, leaving string literals untouched. */ +function stripJsonComments(input: string): string { + return input + .replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : "")) + .replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail ?? (m[0] === '"' ? m : "")); +} + +/** Provider override config (baseUrl, compat) without request auth/headers */ +interface ProviderOverride { + baseUrl?: string; + compat?: Model["compat"]; +} + +interface ProviderRequestConfig { + apiKey?: string; + headers?: Record; + authHeader?: boolean; +} + +export type ResolvedRequestAuth = + | { + ok: true; + apiKey?: string; + headers?: Record; + } + | { + ok: false; + error: string; + }; + +/** Result of loading custom models from models.json */ +interface CustomModelsResult { + models: Model[]; + /** Providers with baseUrl/headers/apiKey overrides for built-in models */ + overrides: Map; + /** Per-model overrides: provider -> modelId -> override */ + modelOverrides: Map>; + error: string | undefined; +} + +function emptyCustomModelsResult(error?: string): CustomModelsResult { + return { models: [], overrides: new Map(), modelOverrides: new Map(), error }; +} + +function mergeCompat( + baseCompat: Model["compat"], + overrideCompat: ModelOverride["compat"], +): Model["compat"] | undefined { + if (!overrideCompat) { + return baseCompat; + } + + const base = baseCompat; + const override = overrideCompat as + | OpenAICompletionsCompat + | OpenAIResponsesCompat + | AnthropicMessagesCompat; + const merged = { ...base, ...override } as + | OpenAICompletionsCompat + | OpenAIResponsesCompat + | AnthropicMessagesCompat; + + const baseCompletions = base as OpenAICompletionsCompat | undefined; + const overrideCompletions = override as OpenAICompletionsCompat; + const mergedCompletions = merged as OpenAICompletionsCompat; + + if (baseCompletions?.openRouterRouting || overrideCompletions.openRouterRouting) { + mergedCompletions.openRouterRouting = { + ...baseCompletions?.openRouterRouting, + ...overrideCompletions.openRouterRouting, + }; + } + + if (baseCompletions?.vercelGatewayRouting || overrideCompletions.vercelGatewayRouting) { + mergedCompletions.vercelGatewayRouting = { + ...baseCompletions?.vercelGatewayRouting, + ...overrideCompletions.vercelGatewayRouting, + }; + } + + return merged as Model["compat"]; +} + +/** + * Deep merge a model override into a model. + * Handles nested objects (cost, compat) by merging rather than replacing. + */ +function applyModelOverride(model: Model, override: ModelOverride): Model { + const result = { ...model }; + + // Simple field overrides + if (override.name !== undefined) { + result.name = override.name; + } + if (override.reasoning !== undefined) { + result.reasoning = override.reasoning; + } + if (override.thinkingLevelMap !== undefined) { + result.thinkingLevelMap = { ...model.thinkingLevelMap, ...override.thinkingLevelMap }; + } + if (override.input !== undefined) { + result.input = override.input; + } + if (override.contextWindow !== undefined) { + result.contextWindow = override.contextWindow; + } + if (override.maxTokens !== undefined) { + result.maxTokens = override.maxTokens; + } + + // Merge cost (partial override) + if (override.cost) { + result.cost = { + input: override.cost.input ?? model.cost.input, + output: override.cost.output ?? model.cost.output, + cacheRead: override.cost.cacheRead ?? model.cost.cacheRead, + cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite, + }; + } + + // Deep merge compat + result.compat = mergeCompat(model.compat, override.compat); + + return result; +} + +/** Clear the config value command cache. Exported for testing. */ +export const clearApiKeyCache = clearConfigValueCache; + +/** + * Model registry - loads and manages models, resolves API keys via AuthStorage. + */ +export class ModelRegistry { + private models: Model[] = []; + private providerRequestConfigs: Map = new Map(); + private modelRequestHeaders: Map> = new Map(); + private registeredProviders: Map = new Map(); + private loadError: string | undefined = undefined; + readonly authStorage: AuthStorage; + private modelsJsonPath: string | undefined; + + private constructor(authStorage: AuthStorage, modelsJsonPath: string | undefined) { + this.authStorage = authStorage; + this.modelsJsonPath = modelsJsonPath; + this.loadModels(); + } + + static create( + authStorage: AuthStorage, + modelsJsonPath: string = join(getAgentDir(), "models.json"), + ): ModelRegistry { + return new ModelRegistry(authStorage, modelsJsonPath); + } + + static inMemory(authStorage: AuthStorage): ModelRegistry { + return new ModelRegistry(authStorage, undefined); + } + + /** + * Reload models from disk (built-in + custom from models.json). + */ + refresh(): void { + this.providerRequestConfigs.clear(); + this.modelRequestHeaders.clear(); + this.loadError = undefined; + + // Ensure dynamic API/OAuth registrations are rebuilt from current provider state. + resetApiProviders(); + resetOAuthProviders(); + + this.loadModels(); + + for (const [providerName, config] of this.registeredProviders.entries()) { + this.applyProviderConfig(providerName, config); + } + } + + /** + * Get any error from loading models.json (undefined if no error). + */ + getError(): string | undefined { + return this.loadError; + } + + private loadModels(): void { + // Load custom models and overrides from models.json + const { + models: customModels, + overrides, + modelOverrides, + error, + } = this.modelsJsonPath + ? this.loadCustomModels(this.modelsJsonPath) + : emptyCustomModelsResult(); + + if (error) { + this.loadError = error; + // Keep built-in models even if custom models failed to load + } + + const builtInModels = this.loadBuiltInModels(overrides, modelOverrides); + let combined = this.mergeCustomModels(builtInModels, customModels); + + // Let OAuth providers modify their models (e.g., update baseUrl) + for (const oauthProvider of this.authStorage.getOAuthProviders()) { + const cred = this.authStorage.get(oauthProvider.id); + if (cred?.type === "oauth" && oauthProvider.modifyModels) { + combined = oauthProvider.modifyModels(combined, cred); + } + } + + this.models = combined; + } + + /** Load built-in models and apply provider/model overrides */ + private loadBuiltInModels( + overrides: Map, + modelOverrides: Map>, + ): Model[] { + return getProviders().flatMap((provider) => { + const models = getModels(provider) as Model[]; + const providerOverride = overrides.get(provider); + const perModelOverrides = modelOverrides.get(provider); + + return models.map((m) => { + let model = m; + + // Apply provider-level baseUrl/headers/compat override + if (providerOverride) { + model = { + ...model, + baseUrl: providerOverride.baseUrl ?? model.baseUrl, + compat: mergeCompat(model.compat, providerOverride.compat), + }; + } + + // Apply per-model override + const modelOverride = perModelOverrides?.get(m.id); + if (modelOverride) { + model = applyModelOverride(model, modelOverride); + } + + return model; + }); + }); + } + + /** Merge custom models into built-in list by provider+id (custom wins on conflicts). */ + private mergeCustomModels(builtInModels: Model[], customModels: Model[]): Model[] { + const merged = [...builtInModels]; + for (const customModel of customModels) { + const existingIndex = merged.findIndex( + (m) => m.provider === customModel.provider && m.id === customModel.id, + ); + if (existingIndex >= 0) { + merged[existingIndex] = customModel; + } else { + merged.push(customModel); + } + } + return merged; + } + + private loadCustomModels(modelsJsonPath: string): CustomModelsResult { + if (!existsSync(modelsJsonPath)) { + return emptyCustomModelsResult(); + } + + try { + const content = readFileSync(modelsJsonPath, "utf-8"); + const parsed = JSON.parse(stripJsonComments(content)) as unknown; + + if (!validateModelsConfig.Check(parsed)) { + const errors = + validateModelsConfig + .Errors(parsed) + .map((error) => ` - ${formatValidationPath(error)}: ${error.message}`) + .join("\n") || "Unknown schema error"; + return emptyCustomModelsResult( + `Invalid models.json schema:\n${errors}\n\nFile: ${modelsJsonPath}`, + ); + } + + const config = parsed; + + // Additional validation + this.validateConfig(config); + + const overrides = new Map(); + const modelOverrides = new Map>(); + + for (const [providerName, providerConfig] of Object.entries(config.providers)) { + if (providerConfig.baseUrl || providerConfig.compat) { + overrides.set(providerName, { + baseUrl: providerConfig.baseUrl, + compat: providerConfig.compat, + }); + } + + this.storeProviderRequestConfig(providerName, providerConfig); + + if (providerConfig.modelOverrides) { + modelOverrides.set(providerName, new Map(Object.entries(providerConfig.modelOverrides))); + for (const [modelId, modelOverride] of Object.entries(providerConfig.modelOverrides)) { + this.storeModelHeaders(providerName, modelId, modelOverride.headers); + } + } + } + + return { models: this.parseModels(config), overrides, modelOverrides, error: undefined }; + } catch (error) { + if (error instanceof SyntaxError) { + return emptyCustomModelsResult( + `Failed to parse models.json: ${error.message}\n\nFile: ${modelsJsonPath}`, + ); + } + return emptyCustomModelsResult( + `Failed to load models.json: ${error instanceof Error ? error.message : String(error)}\n\nFile: ${modelsJsonPath}`, + ); + } + } + + private validateConfig(config: ModelsConfig): void { + const builtInProviders = new Set(getProviders()); + + for (const [providerName, providerConfig] of Object.entries(config.providers)) { + const isBuiltIn = builtInProviders.has(providerName); + const hasProviderApi = !!providerConfig.api; + const models = providerConfig.models ?? []; + const hasModelOverrides = + providerConfig.modelOverrides && Object.keys(providerConfig.modelOverrides).length > 0; + + if (models.length === 0) { + // Override-only config: needs baseUrl, headers, compat, modelOverrides, or some combination. + if ( + !providerConfig.baseUrl && + !providerConfig.headers && + !providerConfig.compat && + !hasModelOverrides + ) { + throw new Error( + `Provider ${providerName}: must specify "baseUrl", "headers", "compat", "modelOverrides", or "models".`, + ); + } + } else if (!isBuiltIn) { + // Non-built-in providers with custom models require endpoint + auth. + if (!providerConfig.baseUrl) { + throw new Error( + `Provider ${providerName}: "baseUrl" is required when defining custom models.`, + ); + } + if (!providerConfig.apiKey) { + throw new Error( + `Provider ${providerName}: "apiKey" is required when defining custom models.`, + ); + } + } + // Built-in providers with custom models: baseUrl/apiKey/api are optional, + // inherited from built-in models. Auth comes from env vars / auth storage. + + for (const modelDef of models) { + const hasModelApi = !!modelDef.api; + + if (!hasProviderApi && !hasModelApi && !isBuiltIn) { + throw new Error( + `Provider ${providerName}, model ${modelDef.id}: no "api" specified. Set at provider or model level.`, + ); + } + // For built-in providers, api is optional — inherited from built-in models. + + if (!modelDef.id) { + throw new Error(`Provider ${providerName}: model missing "id"`); + } + // Validate contextWindow/maxTokens only if provided (they have defaults) + if (modelDef.contextWindow !== undefined && modelDef.contextWindow <= 0) { + throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid contextWindow`); + } + if (modelDef.maxTokens !== undefined && modelDef.maxTokens <= 0) { + throw new Error(`Provider ${providerName}, model ${modelDef.id}: invalid maxTokens`); + } + } + } + } + + private parseModels(config: ModelsConfig): Model[] { + const models: Model[] = []; + const builtInProviders = new Set(getProviders()); + + // Cache built-in defaults (api, baseUrl) per provider, extracted from first model. + const builtInDefaultsCache = new Map(); + const getBuiltInDefaults = ( + providerName: string, + ): { api: string; baseUrl: string } | undefined => { + if (!builtInProviders.has(providerName)) { + return undefined; + } + if (builtInDefaultsCache.has(providerName)) { + return builtInDefaultsCache.get(providerName); + } + const builtIn = getModels(providerName as KnownProvider) as Model[]; + if (builtIn.length === 0) { + return undefined; + } + const defaults = { api: builtIn[0].api, baseUrl: builtIn[0].baseUrl }; + builtInDefaultsCache.set(providerName, defaults); + return defaults; + }; + + for (const [providerName, providerConfig] of Object.entries(config.providers)) { + const modelDefs = providerConfig.models ?? []; + if (modelDefs.length === 0) { + continue; + } // Override-only, no custom models + + const builtInDefaults = getBuiltInDefaults(providerName); + + for (const modelDef of modelDefs) { + const api = modelDef.api ?? providerConfig.api ?? builtInDefaults?.api; + if (!api) { + continue; + } + + const baseUrl = modelDef.baseUrl ?? providerConfig.baseUrl ?? builtInDefaults?.baseUrl; + if (!baseUrl) { + continue; + } + + const compat = mergeCompat(providerConfig.compat, modelDef.compat); + this.storeModelHeaders(providerName, modelDef.id, modelDef.headers); + + const defaultCost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; + models.push({ + id: modelDef.id, + name: modelDef.name ?? modelDef.id, + api: api as Api, + provider: providerName, + baseUrl, + reasoning: modelDef.reasoning ?? false, + thinkingLevelMap: modelDef.thinkingLevelMap, + input: modelDef.input ?? ["text"], + cost: modelDef.cost ?? defaultCost, + contextWindow: modelDef.contextWindow ?? 128000, + maxTokens: modelDef.maxTokens ?? 16384, + headers: undefined, + compat, + } as Model); + } + } + + return models; + } + + /** + * Get all models (built-in + custom). + * If models.json had errors, returns only built-in models. + */ + getAll(): Model[] { + return this.models; + } + + /** + * Get only models that have auth configured. + * This is a fast check that doesn't refresh OAuth tokens. + */ + getAvailable(): Model[] { + return this.models.filter((m) => this.hasConfiguredAuth(m)); + } + + /** + * Find a model by provider and ID. + */ + find(provider: string, modelId: string): Model | undefined { + return this.models.find((m) => m.provider === provider && m.id === modelId); + } + + /** + * Get API key for a model. + */ + hasConfiguredAuth(model: Model): boolean { + return ( + this.authStorage.hasAuth(model.provider) || + this.providerRequestConfigs.get(model.provider)?.apiKey !== undefined + ); + } + + private getModelRequestKey(provider: string, modelId: string): string { + return `${provider}:${modelId}`; + } + + private storeProviderRequestConfig( + providerName: string, + config: { + apiKey?: string; + headers?: Record; + authHeader?: boolean; + }, + ): void { + if (!config.apiKey && !config.headers && !config.authHeader) { + return; + } + + this.providerRequestConfigs.set(providerName, { + apiKey: config.apiKey, + headers: config.headers, + authHeader: config.authHeader, + }); + } + + private storeModelHeaders( + providerName: string, + modelId: string, + headers?: Record, + ): void { + const key = this.getModelRequestKey(providerName, modelId); + if (!headers || Object.keys(headers).length === 0) { + this.modelRequestHeaders.delete(key); + return; + } + this.modelRequestHeaders.set(key, headers); + } + + /** + * Get API key and request headers for a model. + */ + async getApiKeyAndHeaders(model: Model): Promise { + try { + const providerConfig = this.providerRequestConfigs.get(model.provider); + const apiKeyFromAuthStorage = await this.authStorage.getApiKey(model.provider, { + includeFallback: false, + }); + const apiKey = + apiKeyFromAuthStorage ?? + (providerConfig?.apiKey + ? resolveConfigValueOrThrow( + providerConfig.apiKey, + `API key for provider "${model.provider}"`, + ) + : undefined); + + const providerHeaders = resolveHeadersOrThrow( + providerConfig?.headers, + `provider "${model.provider}"`, + ); + const modelHeaders = resolveHeadersOrThrow( + this.modelRequestHeaders.get(this.getModelRequestKey(model.provider, model.id)), + `model "${model.provider}/${model.id}"`, + ); + + let headers = + model.headers || providerHeaders || modelHeaders + ? { ...model.headers, ...providerHeaders, ...modelHeaders } + : undefined; + + if (providerConfig?.authHeader) { + if (!apiKey) { + return { ok: false, error: `No API key found for "${model.provider}"` }; + } + headers = { ...headers, Authorization: `Bearer ${apiKey}` }; + } + + return { + ok: true, + apiKey, + headers: headers && Object.keys(headers).length > 0 ? headers : undefined, + }; + } catch (error) { + return { + ok: false, + error: error instanceof Error ? error.message : String(error), + }; + } + } + + /** + * Return auth status for a provider, including request auth configured in models.json. + * This intentionally does not execute command-backed config values. + */ + getProviderAuthStatus(provider: string): AuthStatus { + const authStatus = this.authStorage.getAuthStatus(provider); + if (authStatus.source) { + return authStatus; + } + + const providerApiKey = this.providerRequestConfigs.get(provider)?.apiKey; + if (!providerApiKey) { + return authStatus; + } + + if (providerApiKey.startsWith("!")) { + return { configured: true, source: "models_json_command" }; + } + + if (process.env[providerApiKey]) { + return { configured: true, source: "environment", label: providerApiKey }; + } + + return { configured: true, source: "models_json_key" }; + } + + /** + * Get display name for a provider. + */ + getProviderDisplayName(provider: string): string { + const registeredProvider = this.registeredProviders.get(provider); + const oauthProvider = this.authStorage.getOAuthProviders().find((p) => p.id === provider); + + return ( + registeredProvider?.name ?? + registeredProvider?.oauth?.name ?? + oauthProvider?.name ?? + BUILT_IN_PROVIDER_DISPLAY_NAMES[provider] ?? + provider + ); + } + + /** + * Get API key for a provider. + */ + async getApiKeyForProvider(provider: string): Promise { + const apiKey = await this.authStorage.getApiKey(provider, { includeFallback: false }); + if (apiKey !== undefined) { + return apiKey; + } + + const providerApiKey = this.providerRequestConfigs.get(provider)?.apiKey; + return providerApiKey ? resolveConfigValueUncached(providerApiKey) : undefined; + } + + /** + * Check if a model is using OAuth credentials (subscription). + */ + isUsingOAuth(model: Model): boolean { + const cred = this.authStorage.get(model.provider); + return cred?.type === "oauth"; + } + + /** + * Register a provider dynamically (from extensions). + * + * If provider has models: replaces all existing models for this provider. + * If provider has only baseUrl/headers: overrides existing models' URLs. + * If provider has oauth: registers OAuth provider for /login support. + */ + registerProvider(providerName: string, config: ProviderConfigInput): void { + this.validateProviderConfig(providerName, config); + this.applyProviderConfig(providerName, config); + this.upsertRegisteredProvider(providerName, config); + } + + /** + * Unregister a previously registered provider. + * + * Removes the provider from the registry and reloads models from disk so that + * built-in models overridden by this provider are restored to their original state. + * Also resets dynamic OAuth and API stream registrations before reapplying + * remaining dynamic providers. + * Has no effect if the provider was never registered. + */ + unregisterProvider(providerName: string): void { + if (!this.registeredProviders.has(providerName)) { + return; + } + this.registeredProviders.delete(providerName); + this.refresh(); + } + + /** + * Upsert a provider config into registeredProviders. + * If the provider is already registered, defined values in the incoming config + * override existing ones; undefined values are preserved from the stored config. + * If the provider is not registered, the incoming config is stored as-is. + */ + private upsertRegisteredProvider(providerName: string, config: ProviderConfigInput): void { + const existing = this.registeredProviders.get(providerName); + if (!existing) { + this.registeredProviders.set(providerName, config); + return; + } + for (const k of Object.keys(config) as (keyof ProviderConfigInput)[]) { + if (config[k] !== undefined) { + (existing as Record)[k] = config[k]; + } + } + } + + private validateProviderConfig(providerName: string, config: ProviderConfigInput): void { + if (config.streamSimple && !config.api) { + throw new Error(`Provider ${providerName}: "api" is required when registering streamSimple.`); + } + + if (!config.models || config.models.length === 0) { + return; + } + + if (!config.baseUrl) { + throw new Error(`Provider ${providerName}: "baseUrl" is required when defining models.`); + } + if (!config.apiKey && !config.oauth) { + throw new Error( + `Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`, + ); + } + + for (const modelDef of config.models) { + const api = modelDef.api || config.api; + if (!api) { + throw new Error(`Provider ${providerName}, model ${modelDef.id}: no "api" specified.`); + } + } + } + + private applyProviderConfig(providerName: string, config: ProviderConfigInput): void { + // Register OAuth provider if provided + if (config.oauth) { + // Ensure the OAuth provider ID matches the provider name + const oauthProvider: OAuthProviderInterface = { + ...config.oauth, + id: providerName, + }; + registerOAuthProvider(oauthProvider); + } + + if (config.streamSimple) { + const streamSimple = config.streamSimple; + registerApiProvider( + { + api: config.api!, + stream: (model, context, options) => + streamSimple(model, context, options as SimpleStreamOptions), + streamSimple, + }, + `provider:${providerName}`, + ); + } + + this.storeProviderRequestConfig(providerName, config); + + if (config.models && config.models.length > 0) { + // Full replacement: remove existing models for this provider + this.models = this.models.filter((m) => m.provider !== providerName); + + // Parse and add new models + for (const modelDef of config.models) { + const api = modelDef.api || config.api; + this.storeModelHeaders(providerName, modelDef.id, modelDef.headers); + + this.models.push({ + id: modelDef.id, + name: modelDef.name, + api: api as Api, + provider: providerName, + baseUrl: modelDef.baseUrl ?? config.baseUrl!, + reasoning: modelDef.reasoning, + thinkingLevelMap: modelDef.thinkingLevelMap, + input: modelDef.input, + cost: modelDef.cost, + contextWindow: modelDef.contextWindow, + maxTokens: modelDef.maxTokens, + headers: undefined, + compat: modelDef.compat, + } as Model); + } + + // Apply OAuth modifyModels if credentials exist (e.g., to update baseUrl) + if (config.oauth?.modifyModels) { + const cred = this.authStorage.get(providerName); + if (cred?.type === "oauth") { + this.models = config.oauth.modifyModels(this.models, cred); + } + } + } else if (config.baseUrl || config.headers) { + // Override-only: update baseUrl for existing models. Request headers are resolved per request. + this.models = this.models.map((m) => { + if (m.provider !== providerName) { + return m; + } + return { + ...m, + baseUrl: config.baseUrl ?? m.baseUrl, + }; + }); + } + } +} + +/** + * Input type for registerProvider API. + */ +export interface ProviderConfigInput { + name?: string; + baseUrl?: string; + apiKey?: string; + api?: Api; + streamSimple?: ( + model: Model, + context: Context, + options?: SimpleStreamOptions, + ) => AssistantMessageEventStream; + headers?: Record; + authHeader?: boolean; + /** OAuth provider for /login support */ + oauth?: Omit; + models?: Array<{ + id: string; + name: string; + api?: Api; + baseUrl?: string; + reasoning: boolean; + thinkingLevelMap?: Model["thinkingLevelMap"]; + input: ("text" | "image")[]; + cost: { input: number; output: number; cacheRead: number; cacheWrite: number }; + contextWindow: number; + maxTokens: number; + headers?: Record; + compat?: Model["compat"]; + }>; +} diff --git a/src/agents/sessions/model-resolver.ts b/src/agents/sessions/model-resolver.ts new file mode 100644 index 00000000000..9e17ef22a71 --- /dev/null +++ b/src/agents/sessions/model-resolver.ts @@ -0,0 +1,676 @@ +/** + * Model resolution, scoping, and initial selection + */ + +import chalk from "chalk"; +import { minimatch } from "minimatch"; +import { type KnownProvider, type Model, modelsAreEqual } from "openclaw/plugin-sdk/llm"; +import type { ThinkingLevel } from "../runtime/index.js"; +import { DEFAULT_THINKING_LEVEL } from "./defaults.js"; +import type { ModelRegistry } from "./model-registry.js"; + +const VALID_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const; + +function isValidThinkingLevel(level: string): level is ThinkingLevel { + return VALID_THINKING_LEVELS.includes(level as ThinkingLevel); +} + +/** Default model IDs for each known provider */ +export const defaultModelPerProvider: Record = { + "amazon-bedrock": "us.anthropic.claude-opus-4-6-v1", + anthropic: "claude-opus-4-7", + openai: "gpt-5.4", + "azure-openai-responses": "gpt-5.4", + "openai-codex": "gpt-5.5", + deepseek: "deepseek-v4-pro", + google: "gemini-3.1-pro-preview", + "google-vertex": "gemini-3.1-pro-preview", + "github-copilot": "gpt-5.4", + openrouter: "moonshotai/kimi-k2.6", + "vercel-ai-gateway": "zai/glm-5.1", + xai: "grok-4.20-0309-reasoning", + groq: "openai/gpt-oss-120b", + cerebras: "zai-glm-4.7", + zai: "glm-5.1", + mistral: "devstral-medium-latest", + minimax: "MiniMax-M2.7", + "minimax-cn": "MiniMax-M2.7", + moonshotai: "kimi-k2.6", + "moonshotai-cn": "kimi-k2.6", + huggingface: "moonshotai/Kimi-K2.6", + fireworks: "accounts/fireworks/models/kimi-k2p6", + together: "moonshotai/Kimi-K2.6", + opencode: "kimi-k2.6", + "opencode-go": "kimi-k2.6", + "kimi-coding": "kimi-for-coding", + "cloudflare-workers-ai": "@cf/moonshotai/kimi-k2.6", + "cloudflare-ai-gateway": "workers-ai/@cf/moonshotai/kimi-k2.6", + xiaomi: "mimo-v2.5-pro", + "xiaomi-token-plan-cn": "mimo-v2.5-pro", + "xiaomi-token-plan-ams": "mimo-v2.5-pro", + "xiaomi-token-plan-sgp": "mimo-v2.5-pro", +}; + +export interface ScopedModel { + model: Model; + /** Thinking level if explicitly specified in pattern (e.g., "model:high"), undefined otherwise */ + thinkingLevel?: ThinkingLevel; +} + +/** + * Helper to check if a model ID looks like an alias (no date suffix) + * Dates are typically in format: -20241022 or -20250929 + */ +function isAlias(id: string): boolean { + // Check if ID ends with -latest + if (id.endsWith("-latest")) { + return true; + } + + // Check if ID ends with a date pattern (-YYYYMMDD) + const datePattern = /-\d{8}$/; + return !datePattern.test(id); +} + +/** + * Find an exact model reference match. + * Supports either a bare model id or a canonical provider/modelId reference. + * When matching by bare id, ambiguous matches across providers are rejected. + */ +export function findExactModelReferenceMatch( + modelReference: string, + availableModels: Model[], +): Model | undefined { + const trimmedReference = modelReference.trim(); + if (!trimmedReference) { + return undefined; + } + + const normalizedReference = trimmedReference.toLowerCase(); + + const canonicalMatches = availableModels.filter( + (model) => `${model.provider}/${model.id}`.toLowerCase() === normalizedReference, + ); + if (canonicalMatches.length === 1) { + return canonicalMatches[0]; + } + if (canonicalMatches.length > 1) { + return undefined; + } + + const slashIndex = trimmedReference.indexOf("/"); + if (slashIndex !== -1) { + const provider = trimmedReference.slice(0, slashIndex).trim(); + const modelId = trimmedReference.slice(slashIndex + 1).trim(); + if (provider && modelId) { + const providerMatches = availableModels.filter( + (model) => + model.provider.toLowerCase() === provider.toLowerCase() && + model.id.toLowerCase() === modelId.toLowerCase(), + ); + if (providerMatches.length === 1) { + return providerMatches[0]; + } + if (providerMatches.length > 1) { + return undefined; + } + } + } + + const idMatches = availableModels.filter( + (model) => model.id.toLowerCase() === normalizedReference, + ); + return idMatches.length === 1 ? idMatches[0] : undefined; +} + +/** + * Try to match a pattern to a model from the available models list. + * Returns the matched model or undefined if no match found. + */ +function tryMatchModel(modelPattern: string, availableModels: Model[]): Model | undefined { + const exactMatch = findExactModelReferenceMatch(modelPattern, availableModels); + if (exactMatch) { + return exactMatch; + } + + // No exact match - fall back to partial matching + const matches = availableModels.filter( + (m) => + m.id.toLowerCase().includes(modelPattern.toLowerCase()) || + m.name?.toLowerCase().includes(modelPattern.toLowerCase()), + ); + + if (matches.length === 0) { + return undefined; + } + + // Separate into aliases and dated versions + const aliases = matches.filter((m) => isAlias(m.id)); + const datedVersions = matches.filter((m) => !isAlias(m.id)); + + if (aliases.length > 0) { + // Prefer alias - if multiple aliases, pick the one that sorts highest + aliases.sort((a, b) => b.id.localeCompare(a.id)); + return aliases[0]; + } + // No alias found, pick latest dated version + datedVersions.sort((a, b) => b.id.localeCompare(a.id)); + return datedVersions[0]; +} + +export interface ParsedModelResult { + model: Model | undefined; + /** Thinking level if explicitly specified in pattern, undefined otherwise */ + thinkingLevel?: ThinkingLevel; + warning: string | undefined; +} + +function buildFallbackModel( + provider: string, + modelId: string, + availableModels: Model[], +): Model | undefined { + const providerModels = availableModels.filter((m) => m.provider === provider); + if (providerModels.length === 0) { + return undefined; + } + + const defaultId = defaultModelPerProvider[provider as KnownProvider]; + const baseModel = defaultId + ? (providerModels.find((m) => m.id === defaultId) ?? providerModels[0]) + : providerModels[0]; + + return { + ...baseModel, + id: modelId, + name: modelId, + }; +} + +/** + * Parse a pattern to extract model and thinking level. + * Handles models with colons in their IDs (e.g., OpenRouter's :exacto suffix). + * + * Algorithm: + * 1. Try to match full pattern as a model + * 2. If found, return it with "off" thinking level + * 3. If not found and has colons, split on last colon: + * - If suffix is valid thinking level, use it and recurse on prefix + * - If suffix is invalid, warn and recurse on prefix with "off" + * + * @internal Exported for testing + */ +export function parseModelPattern( + pattern: string, + availableModels: Model[], + options?: { allowInvalidThinkingLevelFallback?: boolean }, +): ParsedModelResult { + // Try exact match first + const exactMatch = tryMatchModel(pattern, availableModels); + if (exactMatch) { + return { model: exactMatch, thinkingLevel: undefined, warning: undefined }; + } + + // No match - try splitting on last colon if present + const lastColonIndex = pattern.lastIndexOf(":"); + if (lastColonIndex === -1) { + // No colons, pattern simply doesn't match unknown model + return { model: undefined, thinkingLevel: undefined, warning: undefined }; + } + + const prefix = pattern.slice(0, lastColonIndex); + const suffix = pattern.slice(lastColonIndex + 1); + + if (isValidThinkingLevel(suffix)) { + // Valid thinking level - recurse on prefix and use this level + const result = parseModelPattern(prefix, availableModels, options); + if (result.model) { + // Only use this thinking level if no warning from inner recursion + return { + model: result.model, + thinkingLevel: result.warning ? undefined : suffix, + warning: result.warning, + }; + } + return result; + } + // Invalid suffix + const allowFallback = options?.allowInvalidThinkingLevelFallback ?? true; + if (!allowFallback) { + // In strict mode (CLI --model parsing), treat it as part of the model id and fail. + // This avoids accidentally resolving to a different model. + return { model: undefined, thinkingLevel: undefined, warning: undefined }; + } + + // Scope mode: recurse on prefix and warn + const result = parseModelPattern(prefix, availableModels, options); + if (result.model) { + return { + model: result.model, + thinkingLevel: undefined, + warning: `Invalid thinking level "${suffix}" in pattern "${pattern}". Using default instead.`, + }; + } + return result; +} + +/** + * Resolve model patterns to actual Model objects with optional thinking levels + * Format: "pattern:level" where :level is optional + * For each pattern, finds all matching models and picks the best version: + * 1. Prefer alias (e.g., claude-sonnet-4-5) over dated versions (claude-sonnet-4-5-20250929) + * 2. If no alias, pick the latest dated version + * + * Supports models with colons in their IDs (e.g., OpenRouter's model:exacto). + * The algorithm tries to match the full pattern first, then progressively + * strips colon-suffixes to find a match. + */ +export async function resolveModelScope( + patterns: string[], + modelRegistry: ModelRegistry, +): Promise { + const availableModels = modelRegistry.getAvailable(); + const scopedModels: ScopedModel[] = []; + + for (const pattern of patterns) { + // Check if pattern contains glob characters + if (pattern.includes("*") || pattern.includes("?") || pattern.includes("[")) { + // Extract optional thinking level suffix (e.g., "provider/*:high") + const colonIdx = pattern.lastIndexOf(":"); + let globPattern = pattern; + let thinkingLevel: ThinkingLevel | undefined; + + if (colonIdx !== -1) { + const suffix = pattern.slice(colonIdx + 1); + if (isValidThinkingLevel(suffix)) { + thinkingLevel = suffix; + globPattern = pattern.slice(0, colonIdx); + } + } + + // Match against "provider/modelId" format OR just model ID + // This allows "*sonnet*" to match without requiring "anthropic/*sonnet*" + const matchingModels = availableModels.filter((m) => { + const fullId = `${m.provider}/${m.id}`; + return ( + minimatch(fullId, globPattern, { nocase: true }) || + minimatch(m.id, globPattern, { nocase: true }) + ); + }); + + if (matchingModels.length === 0) { + console.warn(chalk.yellow(`Warning: No models match pattern "${pattern}"`)); + continue; + } + + for (const model of matchingModels) { + if (!scopedModels.some((sm) => modelsAreEqual(sm.model, model))) { + scopedModels.push({ model, thinkingLevel }); + } + } + continue; + } + + const { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels); + + if (warning) { + console.warn(chalk.yellow(`Warning: ${warning}`)); + } + + if (!model) { + console.warn(chalk.yellow(`Warning: No models match pattern "${pattern}"`)); + continue; + } + + // Avoid duplicates + if (!scopedModels.some((sm) => modelsAreEqual(sm.model, model))) { + scopedModels.push({ model, thinkingLevel }); + } + } + + return scopedModels; +} + +export interface ResolveCliModelResult { + model: Model | undefined; + thinkingLevel?: ThinkingLevel; + warning: string | undefined; + /** + * Error message suitable for CLI display. + * When set, model will be undefined. + */ + error: string | undefined; +} + +/** + * Resolve a single model from CLI flags. + * + * Supports: + * - --provider --model + * - --model / + * - Fuzzy matching (same rules as model scoping: exact id, then partial id/name) + * + * Note: This does not apply the thinking level by itself, but it may *parse* and + * return a thinking level from ":" so the caller can apply it. + */ +export function resolveCliModel(options: { + cliProvider?: string; + cliModel?: string; + modelRegistry: ModelRegistry; +}): ResolveCliModelResult { + const { cliProvider, cliModel, modelRegistry } = options; + + if (!cliModel) { + return { model: undefined, warning: undefined, error: undefined }; + } + + // Important: use *all* models here, not just models with pre-configured auth. + // This allows "--api-key" to be used for first-time setup. + const availableModels = modelRegistry.getAll(); + if (availableModels.length === 0) { + return { + model: undefined, + warning: undefined, + error: "No models available. Check your installation or add models to models.json.", + }; + } + + // Build canonical provider lookup (case-insensitive) + const providerMap = new Map(); + for (const m of availableModels) { + providerMap.set(m.provider.toLowerCase(), m.provider); + } + + let provider = cliProvider ? providerMap.get(cliProvider.toLowerCase()) : undefined; + if (cliProvider && !provider) { + return { + model: undefined, + warning: undefined, + error: `Unknown provider "${cliProvider}". Use --list-models to see available providers/models.`, + }; + } + + // If no explicit --provider, try to interpret "provider/model" format first. + // When the prefix before the first slash matches a known provider, prefer that + // interpretation over matching models whose IDs literally contain slashes + // (e.g. "zai/glm-5" should resolve to provider=zai, model=glm-5, not to a + // vercel-ai-gateway model with id "zai/glm-5"). + let pattern = cliModel; + let inferredProvider = false; + + if (!provider) { + const slashIndex = cliModel.indexOf("/"); + if (slashIndex !== -1) { + const maybeProvider = cliModel.slice(0, slashIndex); + const canonical = providerMap.get(maybeProvider.toLowerCase()); + if (canonical) { + provider = canonical; + pattern = cliModel.slice(slashIndex + 1); + inferredProvider = true; + } + } + } + + // If no provider was inferred from the slash, try exact matches without provider inference. + // This handles models whose IDs naturally contain slashes (e.g. OpenRouter-style IDs). + if (!provider) { + const lower = cliModel.toLowerCase(); + const exact = availableModels.find( + (m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower, + ); + if (exact) { + return { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined }; + } + } + + if (cliProvider && provider) { + // If both were provided, tolerate --model / by stripping the provider prefix + const prefix = `${provider}/`; + if (cliModel.toLowerCase().startsWith(prefix.toLowerCase())) { + pattern = cliModel.slice(prefix.length); + } + } + + const candidates = provider + ? availableModels.filter((m) => m.provider === provider) + : availableModels; + const { model, thinkingLevel, warning } = parseModelPattern(pattern, candidates, { + allowInvalidThinkingLevelFallback: false, + }); + + if (model) { + return { model, thinkingLevel, warning, error: undefined }; + } + + // If we inferred a provider from the slash but found no match within that provider, + // fall back to matching the full input as a raw model id across all models. + // This handles OpenRouter-style IDs like "openai/gpt-4o:extended" where "openai" + // looks like a provider but the full string is actually a model id on openrouter. + if (inferredProvider) { + const lower = cliModel.toLowerCase(); + const exact = availableModels.find( + (m) => m.id.toLowerCase() === lower || `${m.provider}/${m.id}`.toLowerCase() === lower, + ); + if (exact) { + return { model: exact, warning: undefined, thinkingLevel: undefined, error: undefined }; + } + // Also try parseModelPattern on the full input against all models + const fallback = parseModelPattern(cliModel, availableModels, { + allowInvalidThinkingLevelFallback: false, + }); + if (fallback.model) { + return { + model: fallback.model, + thinkingLevel: fallback.thinkingLevel, + warning: fallback.warning, + error: undefined, + }; + } + } + + if (provider) { + const fallbackModel = buildFallbackModel(provider, pattern, availableModels); + if (fallbackModel) { + const fallbackWarning = warning + ? `${warning} Model "${pattern}" not found for provider "${provider}". Using custom model id.` + : `Model "${pattern}" not found for provider "${provider}". Using custom model id.`; + return { + model: fallbackModel, + thinkingLevel: undefined, + warning: fallbackWarning, + error: undefined, + }; + } + } + + const display = provider ? `${provider}/${pattern}` : cliModel; + return { + model: undefined, + thinkingLevel: undefined, + warning, + error: `Model "${display}" not found. Use --list-models to see available models.`, + }; +} + +export interface InitialModelResult { + model: Model | undefined; + thinkingLevel: ThinkingLevel; + fallbackMessage: string | undefined; +} + +/** + * Find the initial model to use based on priority: + * 1. CLI args (provider + model) + * 2. First model from scoped models (if not continuing/resuming) + * 3. Restored from session (if continuing/resuming) + * 4. Saved default from settings + * 5. First available model with valid API key + */ +export async function findInitialModel(options: { + cliProvider?: string; + cliModel?: string; + scopedModels: ScopedModel[]; + isContinuing: boolean; + defaultProvider?: string; + defaultModelId?: string; + defaultThinkingLevel?: ThinkingLevel; + modelRegistry: ModelRegistry; +}): Promise { + const { + cliProvider, + cliModel, + scopedModels, + isContinuing, + defaultProvider, + defaultModelId, + defaultThinkingLevel, + modelRegistry, + } = options; + + let model: Model | undefined; + let thinkingLevel: ThinkingLevel = DEFAULT_THINKING_LEVEL; + + // 1. CLI args take priority + if (cliProvider && cliModel) { + const resolved = resolveCliModel({ + cliProvider, + cliModel, + modelRegistry, + }); + if (resolved.error) { + console.error(chalk.red(resolved.error)); + process.exit(1); + } + if (resolved.model) { + return { + model: resolved.model, + thinkingLevel: DEFAULT_THINKING_LEVEL, + fallbackMessage: undefined, + }; + } + } + + // 2. Use first model from scoped models (skip if continuing/resuming) + if (scopedModels.length > 0 && !isContinuing) { + return { + model: scopedModels[0].model, + thinkingLevel: + scopedModels[0].thinkingLevel ?? defaultThinkingLevel ?? DEFAULT_THINKING_LEVEL, + fallbackMessage: undefined, + }; + } + + // 3. Try saved default from settings + if (defaultProvider && defaultModelId) { + const found = modelRegistry.find(defaultProvider, defaultModelId); + if (found) { + model = found; + if (defaultThinkingLevel) { + thinkingLevel = defaultThinkingLevel; + } + return { model, thinkingLevel, fallbackMessage: undefined }; + } + } + + // 4. Try first available model with valid API key + const availableModels = modelRegistry.getAvailable(); + + if (availableModels.length > 0) { + // Try to find a default model from known providers + for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) { + const defaultId = defaultModelPerProvider[provider]; + const match = availableModels.find((m) => m.provider === provider && m.id === defaultId); + if (match) { + return { model: match, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined }; + } + } + + // If no default found, use first available + return { + model: availableModels[0], + thinkingLevel: DEFAULT_THINKING_LEVEL, + fallbackMessage: undefined, + }; + } + + // 5. No model found + return { model: undefined, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined }; +} + +/** + * Restore model from session, with fallback to available models + */ +export async function restoreModelFromSession( + savedProvider: string, + savedModelId: string, + currentModel: Model | undefined, + shouldPrintMessages: boolean, + modelRegistry: ModelRegistry, +): Promise<{ model: Model | undefined; fallbackMessage: string | undefined }> { + const restoredModel = modelRegistry.find(savedProvider, savedModelId); + + // Check if restored model exists and still has auth configured + const hasConfiguredAuth = restoredModel ? modelRegistry.hasConfiguredAuth(restoredModel) : false; + + if (restoredModel && hasConfiguredAuth) { + if (shouldPrintMessages) { + console.log(chalk.dim(`Restored model: ${savedProvider}/${savedModelId}`)); + } + return { model: restoredModel, fallbackMessage: undefined }; + } + + // Model not found or no API key - fall back + const reason = !restoredModel ? "model no longer exists" : "no auth configured"; + + if (shouldPrintMessages) { + console.error( + chalk.yellow( + `Warning: Could not restore model ${savedProvider}/${savedModelId} (${reason}).`, + ), + ); + } + + // If we already have a model, use it as fallback + if (currentModel) { + if (shouldPrintMessages) { + console.log(chalk.dim(`Falling back to: ${currentModel.provider}/${currentModel.id}`)); + } + return { + model: currentModel, + fallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${currentModel.provider}/${currentModel.id}.`, + }; + } + + // Try to find any available model + const availableModels = modelRegistry.getAvailable(); + + if (availableModels.length > 0) { + // Try to find a default model from known providers + let fallbackModel: Model | undefined; + for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) { + const defaultId = defaultModelPerProvider[provider]; + const match = availableModels.find((m) => m.provider === provider && m.id === defaultId); + if (match) { + fallbackModel = match; + break; + } + } + + // If no default found, use first available + if (!fallbackModel) { + fallbackModel = availableModels[0]; + } + + if (shouldPrintMessages) { + console.log(chalk.dim(`Falling back to: ${fallbackModel.provider}/${fallbackModel.id}`)); + } + + return { + model: fallbackModel, + fallbackMessage: `Could not restore model ${savedProvider}/${savedModelId} (${reason}). Using ${fallbackModel.provider}/${fallbackModel.id}.`, + }; + } + + // No models available + return { model: undefined, fallbackMessage: undefined }; +} diff --git a/src/agents/sessions/output-guard.ts b/src/agents/sessions/output-guard.ts new file mode 100644 index 00000000000..66555915ff0 --- /dev/null +++ b/src/agents/sessions/output-guard.ts @@ -0,0 +1,84 @@ +interface StdoutTakeoverState { + rawStdoutWrite: (chunk: string, callback?: (error?: Error | null) => void) => boolean; + rawStderrWrite: (chunk: string, callback?: (error?: Error | null) => void) => boolean; + originalStdoutWrite: typeof process.stdout.write; +} + +let stdoutTakeoverState: StdoutTakeoverState | undefined; + +export function takeOverStdout(): void { + if (stdoutTakeoverState) { + return; + } + + const rawStdoutWrite = process.stdout.write.bind( + process.stdout, + ) as StdoutTakeoverState["rawStdoutWrite"]; + const rawStderrWrite = process.stderr.write.bind( + process.stderr, + ) as StdoutTakeoverState["rawStderrWrite"]; + const originalStdoutWrite = process.stdout.write.bind(process.stdout); + + process.stdout.write = (( + chunk: string | Uint8Array, + encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void), + callback?: (error?: Error | null) => void, + ): boolean => { + if (typeof encodingOrCallback === "function") { + return rawStderrWrite(String(chunk), encodingOrCallback); + } + return rawStderrWrite(String(chunk), callback); + }) as typeof process.stdout.write; + + stdoutTakeoverState = { + rawStdoutWrite, + rawStderrWrite, + originalStdoutWrite, + }; +} + +export function restoreStdout(): void { + if (!stdoutTakeoverState) { + return; + } + + process.stdout.write = stdoutTakeoverState.originalStdoutWrite; + stdoutTakeoverState = undefined; +} + +export function isStdoutTakenOver(): boolean { + return stdoutTakeoverState !== undefined; +} + +export function writeRawStdout(text: string): void { + if (stdoutTakeoverState) { + stdoutTakeoverState.rawStdoutWrite(text); + return; + } + process.stdout.write(text); +} + +export async function flushRawStdout(): Promise { + if (stdoutTakeoverState) { + await new Promise((resolve, reject) => { + stdoutTakeoverState?.rawStdoutWrite("", (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); + return; + } + + await new Promise((resolve, reject) => { + process.stdout.write("", (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }); +} diff --git a/src/agents/sessions/package-manager.test.ts b/src/agents/sessions/package-manager.test.ts new file mode 100644 index 00000000000..d6e39e66091 --- /dev/null +++ b/src/agents/sessions/package-manager.test.ts @@ -0,0 +1,106 @@ +import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { DefaultPackageManager } from "./package-manager.js"; +import { SettingsManager } from "./settings-manager.js"; + +const tempDirs: string[] = []; + +async function makeTempDir(prefix: string): Promise { + const dir = await mkdtemp(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); +}); + +describe("DefaultPackageManager", () => { + it("keeps manifest resource entries inside the package root", async () => { + const root = await makeTempDir("openclaw-package-manager-"); + const packageRoot = join(root, "package"); + const outsideRoot = join(root, "outside"); + const insideSkill = join(packageRoot, "skills", "inside", "SKILL.md"); + const outsideSkill = join(outsideRoot, "SKILL.md"); + await mkdir(join(packageRoot, "skills", "inside"), { recursive: true }); + await mkdir(outsideRoot, { recursive: true }); + await writeFile(insideSkill, "# Inside\n", "utf-8"); + await writeFile(outsideSkill, "# Outside\n", "utf-8"); + + const entries = ["skills/inside/SKILL.md", "../outside/SKILL.md", "../outside/*.md"]; + try { + await symlink(outsideRoot, join(packageRoot, "skills", "linked"), "dir"); + entries.push("skills/linked/SKILL.md"); + } catch { + // Some filesystems disallow directory symlinks; path traversal coverage is still enough there. + } + + await writeFile( + join(packageRoot, "package.json"), + JSON.stringify({ openclaw: { skills: entries } }), + "utf-8", + ); + + const manager = new DefaultPackageManager({ + cwd: root, + agentDir: join(root, "agent"), + settingsManager: SettingsManager.inMemory({ packages: [packageRoot] }), + }); + + const resolved = await manager.resolve(); + const skillPaths = resolved.skills.map((skill) => skill.path); + + expect(skillPaths).toContain(insideSkill); + expect(skillPaths).not.toContain(outsideSkill); + }); + + it("keeps convention-discovered resource entries inside the package root", async () => { + const root = await makeTempDir("openclaw-package-manager-"); + const packageRoot = join(root, "package"); + const outsideRoot = join(root, "outside"); + const insideSkill = join(packageRoot, "skills", "inside", "SKILL.md"); + await mkdir(join(packageRoot, "skills", "inside"), { recursive: true }); + await mkdir(outsideRoot, { recursive: true }); + await writeFile(insideSkill, "# Inside\n", "utf-8"); + await writeFile(join(outsideRoot, "SKILL.md"), "# Outside\n", "utf-8"); + await writeFile(join(packageRoot, "package.json"), JSON.stringify({ name: "pkg" }), "utf-8"); + + try { + await symlink(outsideRoot, join(packageRoot, "skills", "linked"), "dir"); + } catch { + // Some filesystems disallow directory symlinks; skip the symlink-only assertion there. + } + + const manager = new DefaultPackageManager({ + cwd: root, + agentDir: join(root, "agent"), + settingsManager: SettingsManager.inMemory({ packages: [packageRoot] }), + }); + + const resolved = await manager.resolve(); + const skillPaths = resolved.skills.map((skill) => skill.path); + + expect(skillPaths).toContain(insideSkill); + expect(skillPaths.some((skillPath) => skillPath.includes(join("skills", "linked")))).toBe( + false, + ); + }); + + it("does not auto-install missing npm package resources", async () => { + const root = await makeTempDir("openclaw-package-manager-"); + const manager = new DefaultPackageManager({ + cwd: root, + agentDir: join(root, "agent"), + settingsManager: SettingsManager.inMemory({ packages: ["npm:@openclaw/missing-test"] }), + }); + + const resolved = await manager.resolve(); + + expect(resolved.extensions).toEqual([]); + expect(resolved.skills).toEqual([]); + expect(resolved.prompts).toEqual([]); + expect(resolved.themes).toEqual([]); + }); +}); diff --git a/src/agents/sessions/package-manager.ts b/src/agents/sessions/package-manager.ts new file mode 100644 index 00000000000..7770393d126 --- /dev/null +++ b/src/agents/sessions/package-manager.ts @@ -0,0 +1,1603 @@ +import { createHash } from "node:crypto"; +import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs"; +import { homedir, tmpdir } from "node:os"; +import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import { globSync } from "glob"; +import ignore from "ignore"; +import { minimatch } from "minimatch"; +import { CONFIG_DIR_NAME } from "../config.js"; +import { type GitSource, parseGitUrl } from "../utils/git.js"; +import { canonicalizePath, isLocalPath } from "../utils/paths.js"; +import type { PackageSource, SettingsManager } from "./settings-manager.js"; + +export interface PathMetadata { + source: string; + scope: SourceScope; + origin: "package" | "top-level"; + baseDir?: string; +} + +export interface ResolvedResource { + path: string; + enabled: boolean; + metadata: PathMetadata; +} + +export interface ResolvedPaths { + extensions: ResolvedResource[]; + skills: ResolvedResource[]; + prompts: ResolvedResource[]; + themes: ResolvedResource[]; +} + +export type MissingSourceAction = "skip" | "error"; + +export interface PackageManager { + resolve(onMissing?: (source: string) => Promise): Promise; + resolveExtensionSources( + sources: string[], + options?: { local?: boolean; temporary?: boolean }, + ): Promise; +} + +interface PackageManagerOptions { + cwd: string; + agentDir: string; + settingsManager: SettingsManager; +} + +type SourceScope = "user" | "project" | "temporary"; + +type NpmSource = { + type: "npm"; + spec: string; + name: string; + pinned: boolean; +}; + +type LocalSource = { + type: "local"; + path: string; +}; + +type ParsedSource = NpmSource | GitSource | LocalSource; + +interface ResourceManifest { + extensions?: string[]; + skills?: string[]; + prompts?: string[]; + themes?: string[]; +} + +interface ResourceAccumulator { + extensions: Map; + skills: Map; + prompts: Map; + themes: Map; +} + +/** + * Compute a numeric precedence rank for a resource based on its metadata. + * Lower rank = higher precedence. Used to sort resolved resources so that + * name-collision resolution ("first wins") produces the correct outcome. + * + * Precedence (highest to lowest): + * 0 project + settings entry (source: "local", scope: "project") + * 1 project + auto-discovered (source: "auto", scope: "project") + * 2 user + settings entry (source: "local", scope: "user") + * 3 user + auto-discovered (source: "auto", scope: "user") + * 4 package resource (origin: "package") + */ +function resourcePrecedenceRank(m: PathMetadata): number { + if (m.origin === "package") { + return 4; + } + const scopeBase = m.scope === "project" ? 0 : 2; + return scopeBase + (m.source === "local" ? 0 : 1); +} + +interface PackageFilter { + extensions?: string[]; + skills?: string[]; + prompts?: string[]; + themes?: string[]; +} + +type ResourceType = "extensions" | "skills" | "prompts" | "themes"; + +const RESOURCE_TYPES: ResourceType[] = ["extensions", "skills", "prompts", "themes"]; + +const FILE_PATTERNS: Record = { + extensions: /\.(ts|js)$/, + skills: /\.md$/, + prompts: /\.md$/, + themes: /\.json$/, +}; + +const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"]; + +type IgnoreMatcher = ReturnType; + +function toPosixPath(p: string): string { + return p.split(sep).join("/"); +} + +function getHomeDir(): string { + return process.env.HOME || homedir(); +} + +function prefixIgnorePattern(line: string, prefix: string): string | null { + const trimmed = line.trim(); + if (!trimmed) { + return null; + } + if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) { + return null; + } + + let pattern = line; + let negated = false; + + if (pattern.startsWith("!")) { + negated = true; + pattern = pattern.slice(1); + } else if (pattern.startsWith("\\!")) { + pattern = pattern.slice(1); + } + + if (pattern.startsWith("/")) { + pattern = pattern.slice(1); + } + + const prefixed = prefix ? `${prefix}${pattern}` : pattern; + return negated ? `!${prefixed}` : prefixed; +} + +function addIgnoreRules(ig: IgnoreMatcher, dir: string, rootDir: string): void { + const relativeDir = relative(rootDir, dir); + const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : ""; + + for (const filename of IGNORE_FILE_NAMES) { + const ignorePath = join(dir, filename); + if (!existsSync(ignorePath)) { + continue; + } + try { + const content = readFileSync(ignorePath, "utf-8"); + const patterns = content + .split(/\r?\n/) + .map((line) => prefixIgnorePattern(line, prefix)) + .filter((line): line is string => Boolean(line)); + if (patterns.length > 0) { + ig.add(patterns); + } + } catch {} + } +} + +function isPattern(s: string): boolean { + return ( + s.startsWith("!") || + s.startsWith("+") || + s.startsWith("-") || + s.includes("*") || + s.includes("?") + ); +} + +function isOverridePattern(s: string): boolean { + return s.startsWith("!") || s.startsWith("+") || s.startsWith("-"); +} + +function hasGlobPattern(s: string): boolean { + return s.includes("*") || s.includes("?"); +} + +function splitPatterns(entries: string[]): { plain: string[]; patterns: string[] } { + const plain: string[] = []; + const patterns: string[] = []; + for (const entry of entries) { + if (isPattern(entry)) { + patterns.push(entry); + } else { + plain.push(entry); + } + } + return { plain, patterns }; +} + +function collectFiles( + dir: string, + filePattern: RegExp, + skipNodeModules = true, + ignoreMatcher?: IgnoreMatcher, + rootDir?: string, +): string[] { + const files: string[] = []; + if (!existsSync(dir)) { + return files; + } + + const root = rootDir ?? dir; + const ig = ignoreMatcher ?? ignore(); + addIgnoreRules(ig, dir, root); + + try { + const entries = readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.name.startsWith(".")) { + continue; + } + if (skipNodeModules && entry.name === "node_modules") { + continue; + } + + const fullPath = join(dir, entry.name); + let isDir = entry.isDirectory(); + let isFile = entry.isFile(); + + if (entry.isSymbolicLink()) { + try { + const stats = statSync(fullPath); + isDir = stats.isDirectory(); + isFile = stats.isFile(); + } catch { + continue; + } + } + + const relPath = toPosixPath(relative(root, fullPath)); + const ignorePath = isDir ? `${relPath}/` : relPath; + if (ig.ignores(ignorePath)) { + continue; + } + + if (isDir) { + files.push(...collectFiles(fullPath, filePattern, skipNodeModules, ig, root)); + } else if (isFile && filePattern.test(entry.name)) { + files.push(fullPath); + } + } + } catch { + // Ignore errors + } + + return files; +} + +type SkillDiscoveryMode = "openclaw" | "agents"; + +function collectSkillEntries( + dir: string, + mode: SkillDiscoveryMode, + ignoreMatcher?: IgnoreMatcher, + rootDir?: string, +): string[] { + const entries: string[] = []; + if (!existsSync(dir)) { + return entries; + } + + const root = rootDir ?? dir; + const ig = ignoreMatcher ?? ignore(); + addIgnoreRules(ig, dir, root); + + try { + const dirEntries = readdirSync(dir, { withFileTypes: true }); + + for (const entry of dirEntries) { + if (entry.name !== "SKILL.md") { + continue; + } + + const fullPath = join(dir, entry.name); + let isFile = entry.isFile(); + if (entry.isSymbolicLink()) { + try { + isFile = statSync(fullPath).isFile(); + } catch { + continue; + } + } + + const relPath = toPosixPath(relative(root, fullPath)); + if (isFile && !ig.ignores(relPath)) { + entries.push(fullPath); + return entries; + } + } + + for (const entry of dirEntries) { + if (entry.name.startsWith(".")) { + continue; + } + if (entry.name === "node_modules") { + continue; + } + + const fullPath = join(dir, entry.name); + let isDir = entry.isDirectory(); + let isFile = entry.isFile(); + + if (entry.isSymbolicLink()) { + try { + const stats = statSync(fullPath); + isDir = stats.isDirectory(); + isFile = stats.isFile(); + } catch { + continue; + } + } + + const relPath = toPosixPath(relative(root, fullPath)); + if ( + mode === "openclaw" && + dir === root && + isFile && + entry.name.endsWith(".md") && + !ig.ignores(relPath) + ) { + entries.push(fullPath); + continue; + } + + if (!isDir) { + continue; + } + if (ig.ignores(`${relPath}/`)) { + continue; + } + + entries.push(...collectSkillEntries(fullPath, mode, ig, root)); + } + } catch { + // Ignore errors + } + + return entries; +} + +function collectAutoSkillEntries(dir: string, mode: SkillDiscoveryMode): string[] { + return collectSkillEntries(dir, mode); +} + +function findGitRepoRoot(startDir: string): string | null { + let dir = resolve(startDir); + while (true) { + if (existsSync(join(dir, ".git"))) { + return dir; + } + const parent = dirname(dir); + if (parent === dir) { + return null; + } + dir = parent; + } +} + +function collectAncestorAgentsSkillDirs(startDir: string): string[] { + const skillDirs: string[] = []; + const resolvedStartDir = resolve(startDir); + const gitRepoRoot = findGitRepoRoot(resolvedStartDir); + + let dir = resolvedStartDir; + while (true) { + skillDirs.push(join(dir, ".agents", "skills")); + if (gitRepoRoot && dir === gitRepoRoot) { + break; + } + const parent = dirname(dir); + if (parent === dir) { + break; + } + dir = parent; + } + + return skillDirs; +} + +function collectAutoPromptEntries(dir: string): string[] { + const entries: string[] = []; + if (!existsSync(dir)) { + return entries; + } + + const ig = ignore(); + addIgnoreRules(ig, dir, dir); + + try { + const dirEntries = readdirSync(dir, { withFileTypes: true }); + for (const entry of dirEntries) { + if (entry.name.startsWith(".")) { + continue; + } + if (entry.name === "node_modules") { + continue; + } + + const fullPath = join(dir, entry.name); + let isFile = entry.isFile(); + if (entry.isSymbolicLink()) { + try { + isFile = statSync(fullPath).isFile(); + } catch { + continue; + } + } + + const relPath = toPosixPath(relative(dir, fullPath)); + if (ig.ignores(relPath)) { + continue; + } + + if (isFile && entry.name.endsWith(".md")) { + entries.push(fullPath); + } + } + } catch { + // Ignore errors + } + + return entries; +} + +function collectAutoThemeEntries(dir: string): string[] { + const entries: string[] = []; + if (!existsSync(dir)) { + return entries; + } + + const ig = ignore(); + addIgnoreRules(ig, dir, dir); + + try { + const dirEntries = readdirSync(dir, { withFileTypes: true }); + for (const entry of dirEntries) { + if (entry.name.startsWith(".")) { + continue; + } + if (entry.name === "node_modules") { + continue; + } + + const fullPath = join(dir, entry.name); + let isFile = entry.isFile(); + if (entry.isSymbolicLink()) { + try { + isFile = statSync(fullPath).isFile(); + } catch { + continue; + } + } + + const relPath = toPosixPath(relative(dir, fullPath)); + if (ig.ignores(relPath)) { + continue; + } + + if (isFile && entry.name.endsWith(".json")) { + entries.push(fullPath); + } + } + } catch { + // Ignore errors + } + + return entries; +} + +function readResourceManifestFile(packageJsonPath: string): ResourceManifest | null { + try { + const content = readFileSync(packageJsonPath, "utf-8"); + const pkg = JSON.parse(content) as { openclaw?: ResourceManifest }; + return pkg.openclaw ?? null; + } catch { + return null; + } +} + +function resolveExtensionEntries(dir: string): string[] | null { + const packageJsonPath = join(dir, "package.json"); + if (existsSync(packageJsonPath)) { + const manifest = readResourceManifestFile(packageJsonPath); + if (manifest?.extensions?.length) { + const entries: string[] = []; + for (const extPath of manifest.extensions) { + const resolvedExtPath = resolve(dir, extPath); + if (existsSync(resolvedExtPath)) { + entries.push(resolvedExtPath); + } + } + if (entries.length > 0) { + return entries; + } + } + } + + const indexTs = join(dir, "index.ts"); + const indexJs = join(dir, "index.js"); + if (existsSync(indexTs)) { + return [indexTs]; + } + if (existsSync(indexJs)) { + return [indexJs]; + } + + return null; +} + +function collectAutoExtensionEntries(dir: string): string[] { + const entries: string[] = []; + if (!existsSync(dir)) { + return entries; + } + + // First check if this directory itself has explicit extension entries (package.json or index) + const rootEntries = resolveExtensionEntries(dir); + if (rootEntries) { + return rootEntries; + } + + // Otherwise, discover extensions from directory contents + const ig = ignore(); + addIgnoreRules(ig, dir, dir); + + try { + const dirEntries = readdirSync(dir, { withFileTypes: true }); + for (const entry of dirEntries) { + if (entry.name.startsWith(".")) { + continue; + } + if (entry.name === "node_modules") { + continue; + } + + const fullPath = join(dir, entry.name); + let isDir = entry.isDirectory(); + let isFile = entry.isFile(); + + if (entry.isSymbolicLink()) { + try { + const stats = statSync(fullPath); + isDir = stats.isDirectory(); + isFile = stats.isFile(); + } catch { + continue; + } + } + + const relPath = toPosixPath(relative(dir, fullPath)); + const ignorePath = isDir ? `${relPath}/` : relPath; + if (ig.ignores(ignorePath)) { + continue; + } + + if (isFile && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) { + entries.push(fullPath); + } else if (isDir) { + const resolvedEntries = resolveExtensionEntries(fullPath); + if (resolvedEntries) { + entries.push(...resolvedEntries); + } + } + } + } catch { + // Ignore errors + } + + return entries; +} + +/** + * Collect resource files from a directory based on resource type. + * Extensions use smart discovery (index.ts in subdirs), others use recursive collection. + */ +function collectResourceFiles(dir: string, resourceType: ResourceType): string[] { + if (resourceType === "skills") { + return collectSkillEntries(dir, "openclaw"); + } + if (resourceType === "extensions") { + return collectAutoExtensionEntries(dir); + } + return collectFiles(dir, FILE_PATTERNS[resourceType]); +} + +function resolveRealPathIfPossible(path: string): string { + try { + return realpathSync.native(path); + } catch { + return resolve(path); + } +} + +function isPathWithinRoot(root: string, candidate: string): boolean { + const rel = relative(root, candidate); + return rel === "" || (rel !== "" && !rel.startsWith("..") && !isAbsolute(rel)); +} + +function matchesAnyPattern(filePath: string, patterns: string[], baseDir: string): boolean { + const rel = toPosixPath(relative(baseDir, filePath)); + const name = basename(filePath); + const filePathPosix = toPosixPath(filePath); + const isSkillFile = name === "SKILL.md"; + const parentDir = isSkillFile ? dirname(filePath) : undefined; + const parentRel = isSkillFile ? toPosixPath(relative(baseDir, parentDir!)) : undefined; + const parentName = isSkillFile ? basename(parentDir!) : undefined; + const parentDirPosix = isSkillFile ? toPosixPath(parentDir!) : undefined; + + return patterns.some((pattern) => { + const normalizedPattern = toPosixPath(pattern); + if ( + minimatch(rel, normalizedPattern) || + minimatch(name, normalizedPattern) || + minimatch(filePathPosix, normalizedPattern) + ) { + return true; + } + if (!isSkillFile) { + return false; + } + return ( + minimatch(parentRel!, normalizedPattern) || + minimatch(parentName!, normalizedPattern) || + minimatch(parentDirPosix!, normalizedPattern) + ); + }); +} + +function normalizeExactPattern(pattern: string): string { + const normalized = + pattern.startsWith("./") || pattern.startsWith(".\\") ? pattern.slice(2) : pattern; + return toPosixPath(normalized); +} + +function matchesAnyExactPattern(filePath: string, patterns: string[], baseDir: string): boolean { + if (patterns.length === 0) { + return false; + } + const rel = toPosixPath(relative(baseDir, filePath)); + const name = basename(filePath); + const filePathPosix = toPosixPath(filePath); + const isSkillFile = name === "SKILL.md"; + const parentDir = isSkillFile ? dirname(filePath) : undefined; + const parentRel = isSkillFile ? toPosixPath(relative(baseDir, parentDir!)) : undefined; + const parentDirPosix = isSkillFile ? toPosixPath(parentDir!) : undefined; + + return patterns.some((pattern) => { + const normalized = normalizeExactPattern(pattern); + if (normalized === rel || normalized === filePathPosix) { + return true; + } + if (!isSkillFile) { + return false; + } + return normalized === parentRel || normalized === parentDirPosix; + }); +} + +function getOverridePatterns(entries: string[]): string[] { + return entries.filter( + (pattern) => pattern.startsWith("!") || pattern.startsWith("+") || pattern.startsWith("-"), + ); +} + +function isEnabledByOverrides(filePath: string, patterns: string[], baseDir: string): boolean { + const overrides = getOverridePatterns(patterns); + const excludes = overrides + .filter((pattern) => pattern.startsWith("!")) + .map((pattern) => pattern.slice(1)); + const forceIncludes = overrides + .filter((pattern) => pattern.startsWith("+")) + .map((pattern) => pattern.slice(1)); + const forceExcludes = overrides + .filter((pattern) => pattern.startsWith("-")) + .map((pattern) => pattern.slice(1)); + + let enabled = true; + if (excludes.length > 0 && matchesAnyPattern(filePath, excludes, baseDir)) { + enabled = false; + } + if (forceIncludes.length > 0 && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) { + enabled = true; + } + if (forceExcludes.length > 0 && matchesAnyExactPattern(filePath, forceExcludes, baseDir)) { + enabled = false; + } + return enabled; +} + +/** + * Apply patterns to paths and return a Set of enabled paths. + * Pattern types: + * - Plain patterns: include matching paths + * - `!pattern`: exclude matching paths + * - `+path`: force-include exact path (overrides exclusions) + * - `-path`: force-exclude exact path (overrides force-includes) + */ +function applyPatterns(allPaths: string[], patterns: string[], baseDir: string): Set { + const includes: string[] = []; + const excludes: string[] = []; + const forceIncludes: string[] = []; + const forceExcludes: string[] = []; + + for (const p of patterns) { + if (p.startsWith("+")) { + forceIncludes.push(p.slice(1)); + } else if (p.startsWith("-")) { + forceExcludes.push(p.slice(1)); + } else if (p.startsWith("!")) { + excludes.push(p.slice(1)); + } else { + includes.push(p); + } + } + + // Step 1: Apply includes (or all if no includes) + let result: string[]; + if (includes.length === 0) { + result = [...allPaths]; + } else { + result = allPaths.filter((filePath) => matchesAnyPattern(filePath, includes, baseDir)); + } + + // Step 2: Apply excludes + if (excludes.length > 0) { + result = result.filter((filePath) => !matchesAnyPattern(filePath, excludes, baseDir)); + } + + // Step 3: Force-include (add back from allPaths, overriding exclusions) + if (forceIncludes.length > 0) { + for (const filePath of allPaths) { + if (!result.includes(filePath) && matchesAnyExactPattern(filePath, forceIncludes, baseDir)) { + result.push(filePath); + } + } + } + + // Step 4: Force-exclude (remove even if included or force-included) + if (forceExcludes.length > 0) { + result = result.filter((filePath) => !matchesAnyExactPattern(filePath, forceExcludes, baseDir)); + } + + return new Set(result); +} + +export class DefaultPackageManager implements PackageManager { + private cwd: string; + private agentDir: string; + private settingsManager: SettingsManager; + + constructor(options: PackageManagerOptions) { + this.cwd = options.cwd; + this.agentDir = options.agentDir; + this.settingsManager = options.settingsManager; + } + + async resolve( + onMissing?: (source: string) => Promise, + ): Promise { + const accumulator = this.createAccumulator(); + const globalSettings = this.settingsManager.getGlobalSettings(); + const projectSettings = this.settingsManager.getProjectSettings(); + + // Collect all packages with scope (project first so cwd resources win collisions) + const allPackages: Array<{ pkg: PackageSource; scope: SourceScope }> = []; + for (const pkg of projectSettings.packages ?? []) { + allPackages.push({ pkg, scope: "project" }); + } + for (const pkg of globalSettings.packages ?? []) { + allPackages.push({ pkg, scope: "user" }); + } + + // Dedupe: project scope wins over global for same package identity + const packageSources = this.dedupePackages(allPackages); + await this.resolvePackageSources(packageSources, accumulator, onMissing); + + const globalBaseDir = this.agentDir; + const projectBaseDir = join(this.cwd, CONFIG_DIR_NAME); + + for (const resourceType of RESOURCE_TYPES) { + const target = this.getTargetMap(accumulator, resourceType); + const globalEntries = globalSettings[resourceType] ?? []; + const projectEntries = projectSettings[resourceType] ?? []; + this.resolveLocalEntries( + projectEntries, + resourceType, + target, + { + source: "local", + scope: "project", + origin: "top-level", + }, + projectBaseDir, + ); + this.resolveLocalEntries( + globalEntries, + resourceType, + target, + { + source: "local", + scope: "user", + origin: "top-level", + }, + globalBaseDir, + ); + } + + this.addAutoDiscoveredResources( + accumulator, + globalSettings, + projectSettings, + globalBaseDir, + projectBaseDir, + ); + + return this.toResolvedPaths(accumulator); + } + + async resolveExtensionSources( + sources: string[], + options?: { local?: boolean; temporary?: boolean }, + ): Promise { + const accumulator = this.createAccumulator(); + const scope: SourceScope = options?.temporary + ? "temporary" + : options?.local + ? "project" + : "user"; + const packageSources = sources.map((source) => ({ pkg: source as PackageSource, scope })); + await this.resolvePackageSources(packageSources, accumulator); + return this.toResolvedPaths(accumulator); + } + + private async resolvePackageSources( + sources: Array<{ pkg: PackageSource; scope: SourceScope }>, + accumulator: ResourceAccumulator, + onMissing?: (source: string) => Promise, + ): Promise { + for (const { pkg, scope } of sources) { + const sourceStr = typeof pkg === "string" ? pkg : pkg.source; + const filter = typeof pkg === "object" ? pkg : undefined; + const parsed = this.parseSource(sourceStr); + const metadata: PathMetadata = { source: sourceStr, scope, origin: "package" }; + + if (parsed.type === "local") { + const baseDir = this.getBaseDirForScope(scope); + this.resolveLocalExtensionSource(parsed, accumulator, filter, metadata, baseDir); + continue; + } + + const handleMissing = async (): Promise => { + if (!onMissing) { + return; + } + const action = await onMissing(sourceStr); + if (action === "error") { + throw new Error(`Missing source: ${sourceStr}`); + } + }; + + if (parsed.type === "npm") { + const installedPath = this.getNpmInstallPath(parsed, scope); + const missingOrWrongVersion = + !existsSync(installedPath) || + (parsed.pinned && !this.installedNpmMatchesPinnedVersion(parsed, installedPath)); + if (missingOrWrongVersion) { + await handleMissing(); + continue; + } + metadata.baseDir = installedPath; + this.collectPackageResources(installedPath, accumulator, filter, metadata); + continue; + } + + if (parsed.type === "git") { + const installedPath = this.getGitInstallPath(parsed, scope); + if (!existsSync(installedPath)) { + await handleMissing(); + continue; + } + metadata.baseDir = installedPath; + this.collectPackageResources(installedPath, accumulator, filter, metadata); + } + } + } + + private resolveLocalExtensionSource( + source: LocalSource, + accumulator: ResourceAccumulator, + filter: PackageFilter | undefined, + metadata: PathMetadata, + baseDir: string, + ): void { + const resolved = this.resolvePathFromBase(source.path, baseDir); + if (!existsSync(resolved)) { + return; + } + + try { + const stats = statSync(resolved); + if (stats.isFile()) { + metadata.baseDir = dirname(resolved); + this.addResource(accumulator.extensions, resolved, metadata, true); + return; + } + if (stats.isDirectory()) { + metadata.baseDir = resolved; + const resources = this.collectPackageResources(resolved, accumulator, filter, metadata); + if (!resources) { + this.addResource(accumulator.extensions, resolved, metadata, true); + } + } + } catch { + return; + } + } + + private parseSource(source: string): ParsedSource { + if (source.startsWith("npm:")) { + const spec = source.slice("npm:".length).trim(); + const { name, version } = this.parseNpmSpec(spec); + return { + type: "npm", + spec, + name, + pinned: Boolean(version), + }; + } + + if (isLocalPath(source)) { + return { type: "local", path: source }; + } + + // Try parsing as git URL + const gitParsed = parseGitUrl(source); + if (gitParsed) { + return gitParsed; + } + + return { type: "local", path: source }; + } + + private installedNpmMatchesPinnedVersion(source: NpmSource, installedPath: string): boolean { + const installedVersion = this.getInstalledNpmVersion(installedPath); + if (!installedVersion) { + return false; + } + + const { version: pinnedVersion } = this.parseNpmSpec(source.spec); + if (!pinnedVersion) { + return true; + } + + return installedVersion === pinnedVersion; + } + + private getInstalledNpmVersion(installedPath: string): string | undefined { + const packageJsonPath = join(installedPath, "package.json"); + if (!existsSync(packageJsonPath)) { + return undefined; + } + try { + const content = readFileSync(packageJsonPath, "utf-8"); + const pkg = JSON.parse(content) as { version?: string }; + return pkg.version; + } catch { + return undefined; + } + } + + /** + * Get a unique identity for a package, ignoring version/ref. + * Used to detect when the same package is in both global and project settings. + * For git packages, uses normalized host/path to ensure SSH and HTTPS URLs + * for the same repository are treated as identical. + */ + private getPackageIdentity(source: string, scope?: SourceScope): string { + const parsed = this.parseSource(source); + if (parsed.type === "npm") { + return `npm:${parsed.name}`; + } + if (parsed.type === "git") { + // Use host/path for identity to normalize SSH and HTTPS + return `git:${parsed.host}/${parsed.path}`; + } + if (scope) { + const baseDir = this.getBaseDirForScope(scope); + return `local:${this.resolvePathFromBase(parsed.path, baseDir)}`; + } + return `local:${this.resolvePath(parsed.path)}`; + } + + /** + * Dedupe packages: if same package identity appears in both global and project, + * keep only the project one (project wins). + */ + private dedupePackages( + packages: Array<{ pkg: PackageSource; scope: SourceScope }>, + ): Array<{ pkg: PackageSource; scope: SourceScope }> { + const seen = new Map(); + + for (const entry of packages) { + const sourceStr = typeof entry.pkg === "string" ? entry.pkg : entry.pkg.source; + const identity = this.getPackageIdentity(sourceStr, entry.scope); + + const existing = seen.get(identity); + if (!existing) { + seen.set(identity, entry); + } else if (entry.scope === "project" && existing.scope === "user") { + // Project wins over user + seen.set(identity, entry); + } + // If existing is project and new is global, keep existing (project) + // If both are same scope, keep first one + } + + return Array.from(seen.values()); + } + + private parseNpmSpec(spec: string): { name: string; version?: string } { + const match = spec.match(/^(@?[^@]+(?:\/[^@]+)?)(?:@(.+))?$/); + if (!match) { + return { name: spec }; + } + const name = match[1] ?? spec; + const version = match[2]; + return { name, version }; + } + + private getNpmInstallPath(source: NpmSource, scope: SourceScope): string { + if (scope === "temporary") { + return join(this.getTemporaryDir("npm"), "node_modules", source.name); + } + if (scope === "project") { + return join(this.cwd, CONFIG_DIR_NAME, "npm", "node_modules", source.name); + } + return join(this.agentDir, "npm", "node_modules", source.name); + } + + private getGitInstallPath(source: GitSource, scope: SourceScope): string { + if (scope === "temporary") { + return this.getTemporaryDir(`git-${source.host}`, source.path); + } + if (scope === "project") { + return join(this.cwd, CONFIG_DIR_NAME, "git", source.host, source.path); + } + return join(this.agentDir, "git", source.host, source.path); + } + + private getTemporaryDir(prefix: string, suffix?: string): string { + const hash = createHash("sha256") + .update(`${prefix}-${suffix ?? ""}`) + .digest("hex") + .slice(0, 8); + return join(tmpdir(), "openclaw-resources", prefix, hash, suffix ?? ""); + } + + private getBaseDirForScope(scope: SourceScope): string { + if (scope === "project") { + return join(this.cwd, CONFIG_DIR_NAME); + } + if (scope === "user") { + return this.agentDir; + } + return this.cwd; + } + + private resolvePath(input: string): string { + const trimmed = input.trim(); + if (trimmed === "~") { + return getHomeDir(); + } + if (trimmed.startsWith("~/")) { + return join(getHomeDir(), trimmed.slice(2)); + } + if (trimmed.startsWith("~")) { + return join(getHomeDir(), trimmed.slice(1)); + } + return resolve(this.cwd, trimmed); + } + + private resolvePathFromBase(input: string, baseDir: string): string { + const trimmed = input.trim(); + if (trimmed === "~") { + return getHomeDir(); + } + if (trimmed.startsWith("~/")) { + return join(getHomeDir(), trimmed.slice(2)); + } + if (trimmed.startsWith("~")) { + return join(getHomeDir(), trimmed.slice(1)); + } + return resolve(baseDir, trimmed); + } + + private collectPackageResources( + packageRoot: string, + accumulator: ResourceAccumulator, + filter: PackageFilter | undefined, + metadata: PathMetadata, + ): boolean { + if (filter) { + for (const resourceType of RESOURCE_TYPES) { + const patterns = filter[resourceType as keyof PackageFilter]; + const target = this.getTargetMap(accumulator, resourceType); + if (patterns !== undefined) { + this.applyPackageFilter(packageRoot, patterns, resourceType, target, metadata); + } else { + this.collectDefaultResources(packageRoot, resourceType, target, metadata); + } + } + return true; + } + + const manifest = this.readResourceManifest(packageRoot); + if (manifest) { + for (const resourceType of RESOURCE_TYPES) { + const entries = manifest[resourceType as keyof ResourceManifest]; + this.addManifestEntries( + entries, + packageRoot, + resourceType, + this.getTargetMap(accumulator, resourceType), + metadata, + ); + } + return true; + } + + let hasAnyDir = false; + for (const resourceType of RESOURCE_TYPES) { + const dir = join(packageRoot, resourceType); + if (existsSync(dir)) { + // Collect all files from the directory (all enabled by default) + const files = this.collectConventionResourceFiles(packageRoot, resourceType); + for (const f of files) { + this.addResource(this.getTargetMap(accumulator, resourceType), f, metadata, true); + } + hasAnyDir = true; + } + } + return hasAnyDir; + } + + private collectDefaultResources( + packageRoot: string, + resourceType: ResourceType, + target: Map, + metadata: PathMetadata, + ): void { + const manifest = this.readResourceManifest(packageRoot); + const entries = manifest?.[resourceType as keyof ResourceManifest]; + if (entries) { + this.addManifestEntries(entries, packageRoot, resourceType, target, metadata); + return; + } + const dir = join(packageRoot, resourceType); + if (existsSync(dir)) { + // Collect all files from the directory (all enabled by default) + const files = this.collectConventionResourceFiles(packageRoot, resourceType); + for (const f of files) { + this.addResource(target, f, metadata, true); + } + } + } + + private applyPackageFilter( + packageRoot: string, + userPatterns: string[], + resourceType: ResourceType, + target: Map, + metadata: PathMetadata, + ): void { + const { allFiles } = this.collectManifestFiles(packageRoot, resourceType); + + if (userPatterns.length === 0) { + // Empty array explicitly disables all resources of this type + for (const f of allFiles) { + this.addResource(target, f, metadata, false); + } + return; + } + + // Apply user patterns + const enabledByUser = applyPatterns(allFiles, userPatterns, packageRoot); + + for (const f of allFiles) { + const enabled = enabledByUser.has(f); + this.addResource(target, f, metadata, enabled); + } + } + + /** + * Collect all files from a package for a resource type, applying manifest patterns. + * Returns { allFiles, enabledByManifest } where enabledByManifest is the set of files + * that pass the manifest's own patterns. + */ + private collectManifestFiles( + packageRoot: string, + resourceType: ResourceType, + ): { allFiles: string[]; enabledByManifest: Set } { + const manifest = this.readResourceManifest(packageRoot); + const entries = manifest?.[resourceType as keyof ResourceManifest]; + if (entries && entries.length > 0) { + const allFiles = this.collectFilesFromManifestEntries(entries, packageRoot, resourceType); + const manifestPatterns = entries.filter(isOverridePattern); + const enabledByManifest = + manifestPatterns.length > 0 + ? applyPatterns(allFiles, manifestPatterns, packageRoot) + : new Set(allFiles); + return { allFiles: Array.from(enabledByManifest), enabledByManifest }; + } + + const allFiles = this.collectConventionResourceFiles(packageRoot, resourceType); + return { allFiles, enabledByManifest: new Set(allFiles) }; + } + + private collectConventionResourceFiles( + packageRoot: string, + resourceType: ResourceType, + ): string[] { + const conventionDir = join(packageRoot, resourceType); + if (!existsSync(conventionDir)) { + return []; + } + return this.filterManifestResourcePaths( + collectResourceFiles(conventionDir, resourceType), + packageRoot, + ); + } + + private readResourceManifest(packageRoot: string): ResourceManifest | null { + const packageJsonPath = join(packageRoot, "package.json"); + if (!existsSync(packageJsonPath)) { + return null; + } + + try { + const content = readFileSync(packageJsonPath, "utf-8"); + const pkg = JSON.parse(content) as { openclaw?: ResourceManifest }; + return pkg.openclaw ?? null; + } catch { + return null; + } + } + + private addManifestEntries( + entries: string[] | undefined, + root: string, + resourceType: ResourceType, + target: Map, + metadata: PathMetadata, + ): void { + if (!entries) { + return; + } + + const allFiles = this.collectFilesFromManifestEntries(entries, root, resourceType); + const patterns = entries.filter(isOverridePattern); + const enabledPaths = applyPatterns(allFiles, patterns, root); + + for (const f of allFiles) { + if (enabledPaths.has(f)) { + this.addResource(target, f, metadata, true); + } + } + } + + private collectFilesFromManifestEntries( + entries: string[], + root: string, + resourceType: ResourceType, + ): string[] { + const sourceEntries = entries.filter((entry) => !isOverridePattern(entry)); + const resolved = sourceEntries.flatMap((entry) => { + if (!hasGlobPattern(entry)) { + return [resolve(root, entry)]; + } + + return globSync(entry, { + cwd: root, + absolute: true, + dot: false, + nodir: false, + }).map((match) => resolve(match)); + }); + return this.collectFilesFromPaths( + this.filterManifestResourcePaths(resolved, root), + resourceType, + ); + } + + private filterManifestResourcePaths(paths: string[], root: string): string[] { + const resolvedRoot = resolve(root); + const realRoot = resolveRealPathIfPossible(resolvedRoot); + return paths.filter((path) => { + const resolvedPath = resolve(path); + if (!isPathWithinRoot(resolvedRoot, resolvedPath)) { + return false; + } + return isPathWithinRoot(realRoot, resolveRealPathIfPossible(resolvedPath)); + }); + } + + private resolveLocalEntries( + entries: string[], + resourceType: ResourceType, + target: Map, + metadata: PathMetadata, + baseDir: string, + ): void { + if (entries.length === 0) { + return; + } + + // Collect all files from plain entries (non-pattern entries) + const { plain, patterns } = splitPatterns(entries); + const resolvedPlain = plain.map((p) => this.resolvePathFromBase(p, baseDir)); + const allFiles = this.collectFilesFromPaths(resolvedPlain, resourceType); + + // Determine which files are enabled based on patterns + const enabledPaths = applyPatterns(allFiles, patterns, baseDir); + + // Add all files with their enabled state + for (const f of allFiles) { + this.addResource(target, f, metadata, enabledPaths.has(f)); + } + } + + private addAutoDiscoveredResources( + accumulator: ResourceAccumulator, + globalSettings: ReturnType, + projectSettings: ReturnType, + globalBaseDir: string, + projectBaseDir: string, + ): void { + const userMetadata: PathMetadata = { + source: "auto", + scope: "user", + origin: "top-level", + baseDir: globalBaseDir, + }; + const projectMetadata: PathMetadata = { + source: "auto", + scope: "project", + origin: "top-level", + baseDir: projectBaseDir, + }; + + const userOverrides = { + extensions: globalSettings.extensions ?? [], + skills: globalSettings.skills ?? [], + prompts: globalSettings.prompts ?? [], + themes: globalSettings.themes ?? [], + }; + const projectOverrides = { + extensions: projectSettings.extensions ?? [], + skills: projectSettings.skills ?? [], + prompts: projectSettings.prompts ?? [], + themes: projectSettings.themes ?? [], + }; + + const userDirs = { + extensions: join(globalBaseDir, "extensions"), + skills: join(globalBaseDir, "skills"), + prompts: join(globalBaseDir, "prompts"), + themes: join(globalBaseDir, "themes"), + }; + const projectDirs = { + extensions: join(projectBaseDir, "extensions"), + skills: join(projectBaseDir, "skills"), + prompts: join(projectBaseDir, "prompts"), + themes: join(projectBaseDir, "themes"), + }; + const userAgentsSkillsDir = join(getHomeDir(), ".agents", "skills"); + const projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd).filter( + (dir) => resolve(dir) !== resolve(userAgentsSkillsDir), + ); + + const addResources = ( + resourceType: ResourceType, + paths: string[], + metadata: PathMetadata, + overrides: string[], + baseDir: string, + ) => { + const target = this.getTargetMap(accumulator, resourceType); + for (const path of paths) { + const enabled = isEnabledByOverrides(path, overrides, baseDir); + this.addResource(target, path, metadata, enabled); + } + }; + + // Project extensions from the embedded agent project directory. + addResources( + "extensions", + collectAutoExtensionEntries(projectDirs.extensions), + projectMetadata, + projectOverrides.extensions, + projectBaseDir, + ); + + // Project skills from the embedded agent project directory. + addResources( + "skills", + collectAutoSkillEntries(projectDirs.skills, "openclaw"), + projectMetadata, + projectOverrides.skills, + projectBaseDir, + ); + + // Project skills from .agents/ (each with its own baseDir) + for (const agentsSkillsDir of projectAgentsSkillDirs) { + const agentsBaseDir = dirname(agentsSkillsDir); // the .agents directory + const agentsMetadata: PathMetadata = { + ...projectMetadata, + baseDir: agentsBaseDir, + }; + addResources( + "skills", + collectAutoSkillEntries(agentsSkillsDir, "agents"), + agentsMetadata, + projectOverrides.skills, + agentsBaseDir, + ); + } + + addResources( + "prompts", + collectAutoPromptEntries(projectDirs.prompts), + projectMetadata, + projectOverrides.prompts, + projectBaseDir, + ); + addResources( + "themes", + collectAutoThemeEntries(projectDirs.themes), + projectMetadata, + projectOverrides.themes, + projectBaseDir, + ); + + // User extensions from ~/.openclaw/agent/ + addResources( + "extensions", + collectAutoExtensionEntries(userDirs.extensions), + userMetadata, + userOverrides.extensions, + globalBaseDir, + ); + + // User skills from ~/.openclaw/agent/ + addResources( + "skills", + collectAutoSkillEntries(userDirs.skills, "openclaw"), + userMetadata, + userOverrides.skills, + globalBaseDir, + ); + + // User skills from ~/.agents/ (with its own baseDir) + const userAgentsBaseDir = dirname(userAgentsSkillsDir); + const userAgentsMetadata: PathMetadata = { + ...userMetadata, + baseDir: userAgentsBaseDir, + }; + addResources( + "skills", + collectAutoSkillEntries(userAgentsSkillsDir, "agents"), + userAgentsMetadata, + userOverrides.skills, + userAgentsBaseDir, + ); + + addResources( + "prompts", + collectAutoPromptEntries(userDirs.prompts), + userMetadata, + userOverrides.prompts, + globalBaseDir, + ); + addResources( + "themes", + collectAutoThemeEntries(userDirs.themes), + userMetadata, + userOverrides.themes, + globalBaseDir, + ); + } + + private collectFilesFromPaths(paths: string[], resourceType: ResourceType): string[] { + const files: string[] = []; + for (const p of paths) { + if (!existsSync(p)) { + continue; + } + + try { + const stats = statSync(p); + if (stats.isFile()) { + files.push(p); + } else if (stats.isDirectory()) { + files.push(...collectResourceFiles(p, resourceType)); + } + } catch { + // Ignore errors + } + } + return files; + } + + private getTargetMap( + accumulator: ResourceAccumulator, + resourceType: ResourceType, + ): Map { + switch (resourceType) { + case "extensions": + return accumulator.extensions; + case "skills": + return accumulator.skills; + case "prompts": + return accumulator.prompts; + case "themes": + return accumulator.themes; + default: + throw new Error(`Unknown resource type: ${String(resourceType)}`); + } + } + + private addResource( + map: Map, + path: string, + metadata: PathMetadata, + enabled: boolean, + ): void { + if (!path) { + return; + } + if (!map.has(path)) { + map.set(path, { metadata, enabled }); + } + } + + private createAccumulator(): ResourceAccumulator { + return { + extensions: new Map(), + skills: new Map(), + prompts: new Map(), + themes: new Map(), + }; + } + + private toResolvedPaths(accumulator: ResourceAccumulator): ResolvedPaths { + const mapToResolved = ( + entries: Map, + ): ResolvedResource[] => { + const resolved = Array.from(entries.entries()).map(([path, { metadata, enabled }]) => ({ + path, + enabled, + metadata, + })); + resolved.sort( + (a, b) => resourcePrecedenceRank(a.metadata) - resourcePrecedenceRank(b.metadata), + ); + + const seen = new Set(); + return resolved.filter((entry) => { + const canonicalPath = canonicalizePath(entry.path); + if (seen.has(canonicalPath)) { + return false; + } + seen.add(canonicalPath); + return true; + }); + }; + + return { + extensions: mapToResolved(accumulator.extensions), + skills: mapToResolved(accumulator.skills), + prompts: mapToResolved(accumulator.prompts), + themes: mapToResolved(accumulator.themes), + }; + } +} diff --git a/src/agents/sessions/prompt-templates.ts b/src/agents/sessions/prompt-templates.ts new file mode 100644 index 00000000000..b6eed709a5a --- /dev/null +++ b/src/agents/sessions/prompt-templates.ts @@ -0,0 +1,315 @@ +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path"; +import { CONFIG_DIR_NAME } from "../config.js"; +import { parseFrontmatter } from "../utils/frontmatter.js"; +import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js"; + +/** + * Represents a prompt template loaded from a markdown file + */ +export interface PromptTemplate { + name: string; + description: string; + argumentHint?: string; + content: string; + sourceInfo: SourceInfo; + filePath: string; // Absolute path to the template file +} + +/** + * Parse command arguments respecting quoted strings (bash-style) + * Returns array of arguments + */ +export function parseCommandArgs(argsString: string): string[] { + const args: string[] = []; + let current = ""; + let inQuote: string | null = null; + + for (let i = 0; i < argsString.length; i++) { + const char = argsString[i]; + + if (inQuote) { + if (char === inQuote) { + inQuote = null; + } else { + current += char; + } + } else if (char === '"' || char === "'") { + inQuote = char; + } else if (/\s/.test(char)) { + if (current) { + args.push(current); + current = ""; + } + } else { + current += char; + } + } + + if (current) { + args.push(current); + } + + return args; +} + +/** + * Substitute argument placeholders in template content + * Supports: + * - $1, $2, ... for positional args + * - $@ and $ARGUMENTS for all args + * - ${@:N} for args from Nth onwards (bash-style slicing) + * - ${@:N:L} for L args starting from Nth + * + * Note: Replacement happens on the template string only. Argument values + * containing patterns like $1, $@, or $ARGUMENTS are NOT recursively substituted. + */ +export function substituteArgs(content: string, args: string[]): string { + let result = content; + + // Replace $1, $2, etc. with positional args FIRST (before wildcards) + // This prevents wildcard replacement values containing $ patterns from being re-substituted + result = result.replace(/\$(\d+)/g, (_, num) => { + const index = Number.parseInt(num, 10) - 1; + return args[index] ?? ""; + }); + + // Replace ${@:start} or ${@:start:length} with sliced args (bash-style) + // Process BEFORE simple $@ to avoid conflicts + result = result.replace(/\$\{@:(\d+)(?::(\d+))?\}/g, (_, startStr, lengthStr) => { + let start = Number.parseInt(startStr, 10) - 1; // Convert to 0-indexed (user provides 1-indexed) + // Treat 0 as 1 (bash convention: args start at 1) + if (start < 0) { + start = 0; + } + + if (lengthStr) { + const length = Number.parseInt(lengthStr, 10); + return args.slice(start, start + length).join(" "); + } + return args.slice(start).join(" "); + }); + + // Pre-compute all args joined (optimization) + const allArgs = args.join(" "); + + // Replace $ARGUMENTS with all args joined (new syntax, aligns with Claude, Codex, OpenCode) + result = result.replace(/\$ARGUMENTS/g, allArgs); + + // Replace $@ with all args joined (existing syntax) + result = result.replace(/\$@/g, allArgs); + + return result; +} + +function loadTemplateFromFile(filePath: string, sourceInfo: SourceInfo): PromptTemplate | null { + try { + const rawContent = readFileSync(filePath, "utf-8"); + const { frontmatter, body } = parseFrontmatter>(rawContent); + + const name = basename(filePath).replace(/\.md$/, ""); + + // Get description from frontmatter or first non-empty line + let description = frontmatter.description || ""; + if (!description) { + const firstLine = body.split("\n").find((line) => line.trim()); + if (firstLine) { + // Truncate if too long + description = firstLine.slice(0, 60); + if (firstLine.length > 60) { + description += "..."; + } + } + } + + return { + name, + description, + ...(frontmatter["argument-hint"] && { argumentHint: frontmatter["argument-hint"] }), + content: body, + sourceInfo, + filePath, + }; + } catch { + return null; + } +} + +/** + * Scan a directory for .md files (non-recursive) and load them as prompt templates. + */ +function loadTemplatesFromDir( + dir: string, + getSourceInfo: (filePath: string) => SourceInfo, +): PromptTemplate[] { + const templates: PromptTemplate[] = []; + + if (!existsSync(dir)) { + return templates; + } + + try { + const entries = readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = join(dir, entry.name); + + // For symlinks, check if they point to a file + let isFile = entry.isFile(); + if (entry.isSymbolicLink()) { + try { + const stats = statSync(fullPath); + isFile = stats.isFile(); + } catch { + // Broken symlink, skip it + continue; + } + } + + if (isFile && entry.name.endsWith(".md")) { + const template = loadTemplateFromFile(fullPath, getSourceInfo(fullPath)); + if (template) { + templates.push(template); + } + } + } + } catch { + return templates; + } + + return templates; +} + +export interface LoadPromptTemplatesOptions { + /** Working directory for project-local templates. */ + cwd: string; + /** Agent config directory for global templates. */ + agentDir: string; + /** Explicit prompt template paths (files or directories). */ + promptPaths: string[]; + /** Include default prompt directories. */ + includeDefaults: boolean; +} + +function normalizePath(input: string): string { + const trimmed = input.trim(); + if (trimmed === "~") { + return homedir(); + } + if (trimmed.startsWith("~/")) { + return join(homedir(), trimmed.slice(2)); + } + if (trimmed.startsWith("~")) { + return join(homedir(), trimmed.slice(1)); + } + return trimmed; +} + +function resolvePromptPath(p: string, cwd: string): string { + const normalized = normalizePath(p); + return isAbsolute(normalized) ? normalized : resolve(cwd, normalized); +} + +/** + * Load all prompt templates from: + * 1. Global: agentDir/prompts/ + * 2. Project: cwd/{CONFIG_DIR_NAME}/prompts/ + * 3. Explicit prompt paths + */ +export function loadPromptTemplates(options: LoadPromptTemplatesOptions): PromptTemplate[] { + const resolvedCwd = options.cwd; + const resolvedAgentDir = options.agentDir; + const promptPaths = options.promptPaths; + const includeDefaults = options.includeDefaults; + + const templates: PromptTemplate[] = []; + + const globalPromptsDir = options.agentDir ? join(options.agentDir, "prompts") : resolvedAgentDir; + const projectPromptsDir = resolve(resolvedCwd, CONFIG_DIR_NAME, "prompts"); + + const isUnderPath = (target: string, root: string): boolean => { + const normalizedRoot = resolve(root); + if (target === normalizedRoot) { + return true; + } + const prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`; + return target.startsWith(prefix); + }; + + const getSourceInfo = (resolvedPath: string): SourceInfo => { + if (isUnderPath(resolvedPath, globalPromptsDir)) { + return createSyntheticSourceInfo(resolvedPath, { + source: "local", + scope: "user", + baseDir: globalPromptsDir, + }); + } + if (isUnderPath(resolvedPath, projectPromptsDir)) { + return createSyntheticSourceInfo(resolvedPath, { + source: "local", + scope: "project", + baseDir: projectPromptsDir, + }); + } + return createSyntheticSourceInfo(resolvedPath, { + source: "local", + baseDir: statSync(resolvedPath).isDirectory() ? resolvedPath : dirname(resolvedPath), + }); + }; + + if (includeDefaults) { + templates.push(...loadTemplatesFromDir(globalPromptsDir, getSourceInfo)); + templates.push(...loadTemplatesFromDir(projectPromptsDir, getSourceInfo)); + } + + // 3. Load explicit prompt paths + for (const rawPath of promptPaths) { + const resolvedPath = resolvePromptPath(rawPath, resolvedCwd); + if (!existsSync(resolvedPath)) { + continue; + } + + try { + const stats = statSync(resolvedPath); + if (stats.isDirectory()) { + templates.push(...loadTemplatesFromDir(resolvedPath, getSourceInfo)); + } else if (stats.isFile() && resolvedPath.endsWith(".md")) { + const template = loadTemplateFromFile(resolvedPath, getSourceInfo(resolvedPath)); + if (template) { + templates.push(template); + } + } + } catch { + // Ignore read failures + } + } + + return templates; +} + +/** + * Expand a prompt template if it matches a template name. + * Returns the expanded content or the original text if not a template. + */ +export function expandPromptTemplate(text: string, templates: PromptTemplate[]): string { + if (!text.startsWith("/")) { + return text; + } + + const match = text.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/); + if (!match) { + return text; + } + + const templateName = match[1]; + const argsString = match[2] ?? ""; + + const template = templates.find((t) => t.name === templateName); + if (template) { + const args = parseCommandArgs(argsString); + return substituteArgs(template.content, args); + } + + return text; +} diff --git a/src/agents/sessions/provider-display-names.ts b/src/agents/sessions/provider-display-names.ts new file mode 100644 index 00000000000..0bb570be258 --- /dev/null +++ b/src/agents/sessions/provider-display-names.ts @@ -0,0 +1,32 @@ +export const BUILT_IN_PROVIDER_DISPLAY_NAMES: Record = { + anthropic: "Anthropic", + "amazon-bedrock": "Amazon Bedrock", + "azure-openai-responses": "Azure OpenAI Responses", + cerebras: "Cerebras", + "cloudflare-ai-gateway": "Cloudflare AI Gateway", + "cloudflare-workers-ai": "Cloudflare Workers AI", + deepseek: "DeepSeek", + fireworks: "Fireworks", + google: "Google Gemini", + "google-vertex": "Google Vertex AI", + groq: "Groq", + huggingface: "Hugging Face", + "kimi-coding": "Kimi For Coding", + mistral: "Mistral", + minimax: "MiniMax", + "minimax-cn": "MiniMax (China)", + moonshotai: "Moonshot AI", + "moonshotai-cn": "Moonshot AI (China)", + opencode: "OpenCode Zen", + "opencode-go": "OpenCode Go", + openai: "OpenAI", + openrouter: "OpenRouter", + together: "Together AI", + "vercel-ai-gateway": "Vercel AI Gateway", + xai: "xAI", + zai: "ZAI", + xiaomi: "Xiaomi MiMo", + "xiaomi-token-plan-cn": "Xiaomi MiMo Token Plan (China)", + "xiaomi-token-plan-ams": "Xiaomi MiMo Token Plan (Amsterdam)", + "xiaomi-token-plan-sgp": "Xiaomi MiMo Token Plan (Singapore)", +}; diff --git a/src/agents/sessions/resolve-config-value.ts b/src/agents/sessions/resolve-config-value.ts new file mode 100644 index 00000000000..fe91e32e12e --- /dev/null +++ b/src/agents/sessions/resolve-config-value.ts @@ -0,0 +1,153 @@ +/** + * Resolve configuration values that may be shell commands, environment variables, or literals. + * Used by auth-storage.ts and model-registry.ts. + */ + +import { execSync, spawnSync } from "node:child_process"; +import { getShellConfig } from "../utils/shell.js"; + +// Cache for shell command results (persists for process lifetime) +const commandResultCache = new Map(); + +/** + * Resolve a config value (API key, header value, etc.) to an actual value. + * - If starts with "!", executes the rest as a shell command and uses stdout (cached) + * - Otherwise checks environment variable first, then treats as literal (not cached) + */ +export function resolveConfigValue(config: string): string | undefined { + if (config.startsWith("!")) { + return executeCommand(config); + } + const envValue = process.env[config]; + return envValue || config; +} + +function executeWithConfiguredShell(command: string): { + executed: boolean; + value: string | undefined; +} { + try { + const { shell, args } = getShellConfig(); + const result = spawnSync(shell, [...args, command], { + encoding: "utf-8", + timeout: 10000, + stdio: ["ignore", "pipe", "ignore"], + shell: false, + windowsHide: true, + }); + + if (result.error) { + const error = result.error as NodeJS.ErrnoException; + if (error.code === "ENOENT") { + return { executed: false, value: undefined }; + } + return { executed: true, value: undefined }; + } + + if (result.status !== 0) { + return { executed: true, value: undefined }; + } + + const value = (result.stdout ?? "").trim(); + return { executed: true, value: value || undefined }; + } catch { + return { executed: false, value: undefined }; + } +} + +function executeWithDefaultShell(command: string): string | undefined { + try { + const output = execSync(command, { + encoding: "utf-8", + timeout: 10000, + stdio: ["ignore", "pipe", "ignore"], + }); + return output.trim() || undefined; + } catch { + return undefined; + } +} + +function executeCommandUncached(commandConfig: string): string | undefined { + const command = commandConfig.slice(1); + return process.platform === "win32" + ? (() => { + const configuredResult = executeWithConfiguredShell(command); + return configuredResult.executed + ? configuredResult.value + : executeWithDefaultShell(command); + })() + : executeWithDefaultShell(command); +} + +function executeCommand(commandConfig: string): string | undefined { + if (commandResultCache.has(commandConfig)) { + return commandResultCache.get(commandConfig); + } + + const result = executeCommandUncached(commandConfig); + commandResultCache.set(commandConfig, result); + return result; +} + +/** + * Resolve all header values using the same resolution logic as API keys. + */ +export function resolveConfigValueUncached(config: string): string | undefined { + if (config.startsWith("!")) { + return executeCommandUncached(config); + } + const envValue = process.env[config]; + return envValue || config; +} + +export function resolveConfigValueOrThrow(config: string, description: string): string { + const resolvedValue = resolveConfigValueUncached(config); + if (resolvedValue !== undefined) { + return resolvedValue; + } + + if (config.startsWith("!")) { + throw new Error(`Failed to resolve ${description} from shell command: ${config.slice(1)}`); + } + + throw new Error(`Failed to resolve ${description}`); +} + +/** + * Resolve all header values using the same resolution logic as API keys. + */ +export function resolveHeaders( + headers: Record | undefined, +): Record | undefined { + if (!headers) { + return undefined; + } + const resolved: Record = {}; + for (const [key, value] of Object.entries(headers)) { + const resolvedValue = resolveConfigValue(value); + if (resolvedValue) { + resolved[key] = resolvedValue; + } + } + return Object.keys(resolved).length > 0 ? resolved : undefined; +} + +export function resolveHeadersOrThrow( + headers: Record | undefined, + description: string, +): Record | undefined { + if (!headers) { + return undefined; + } + const resolved: Record = {}; + for (const [key, value] of Object.entries(headers)) { + resolved[key] = resolveConfigValueOrThrow(value, `${description} header "${key}"`); + } + return Object.keys(resolved).length > 0 ? resolved : undefined; +} + +/** Clear the config value command cache. Exported for testing. */ +export function clearConfigValueCache(): void { + commandResultCache.clear(); +} diff --git a/src/agents/sessions/resource-loader.ts b/src/agents/sessions/resource-loader.ts new file mode 100644 index 00000000000..bd39cfa1849 --- /dev/null +++ b/src/agents/sessions/resource-loader.ts @@ -0,0 +1,1028 @@ +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import { join, resolve, sep } from "node:path"; +import chalk from "chalk"; +import { CONFIG_DIR_NAME } from "../config.js"; +import { loadThemeFromPath, type Theme } from "../modes/interactive/theme/theme.js"; +import type { ResourceDiagnostic } from "./diagnostics.js"; + +export type { ResourceCollision, ResourceDiagnostic } from "./diagnostics.js"; + +import { canonicalizePath, isLocalPath } from "../utils/paths.js"; +import { createEventBus, type EventBus } from "./event-bus.js"; +import { + createExtensionRuntime, + loadExtensionFromFactory, + loadExtensions, +} from "./extensions/loader.js"; +import type { + Extension, + ExtensionFactory, + ExtensionRuntime, + LoadExtensionsResult, +} from "./extensions/types.js"; +import { DefaultPackageManager, type PathMetadata } from "./package-manager.js"; +import type { PromptTemplate } from "./prompt-templates.js"; +import { loadPromptTemplates } from "./prompt-templates.js"; +import { SettingsManager } from "./settings-manager.js"; +import type { Skill } from "./skills.js"; +import { loadSkills } from "./skills.js"; +import { createSourceInfo, type SourceInfo } from "./source-info.js"; + +export interface ResourceExtensionPaths { + skillPaths?: Array<{ path: string; metadata: PathMetadata }>; + promptPaths?: Array<{ path: string; metadata: PathMetadata }>; + themePaths?: Array<{ path: string; metadata: PathMetadata }>; +} + +export interface ResourceLoader { + getExtensions(): LoadExtensionsResult; + getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] }; + getPrompts(): { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] }; + getThemes(): { themes: Theme[]; diagnostics: ResourceDiagnostic[] }; + getAgentsFiles(): { agentsFiles: Array<{ path: string; content: string }> }; + getSystemPrompt(): string | undefined; + getAppendSystemPrompt(): string[]; + extendResources(paths: ResourceExtensionPaths): void; + reload(): Promise; +} + +function resolvePromptInput(input: string | undefined, description: string): string | undefined { + if (!input) { + return undefined; + } + + if (existsSync(input)) { + try { + return readFileSync(input, "utf-8"); + } catch (error) { + console.error( + chalk.yellow(`Warning: Could not read ${description} file ${input}: ${String(error)}`), + ); + return input; + } + } + + return input; +} + +function loadContextFileFromDir(dir: string): { path: string; content: string } | null { + const candidates = ["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]; + for (const filename of candidates) { + const filePath = join(dir, filename); + if (existsSync(filePath)) { + try { + return { + path: filePath, + content: readFileSync(filePath, "utf-8"), + }; + } catch (error) { + console.error(chalk.yellow(`Warning: Could not read ${filePath}: ${String(error)}`)); + } + } + } + return null; +} + +export function loadProjectContextFiles(options: { + cwd: string; + agentDir: string; +}): Array<{ path: string; content: string }> { + const resolvedCwd = options.cwd; + const resolvedAgentDir = options.agentDir; + + const contextFiles: Array<{ path: string; content: string }> = []; + const seenPaths = new Set(); + + const globalContext = loadContextFileFromDir(resolvedAgentDir); + if (globalContext) { + contextFiles.push(globalContext); + seenPaths.add(globalContext.path); + } + + const ancestorContextFiles: Array<{ path: string; content: string }> = []; + + let currentDir = resolvedCwd; + const root = resolve("/"); + + while (true) { + const contextFile = loadContextFileFromDir(currentDir); + if (contextFile && !seenPaths.has(contextFile.path)) { + ancestorContextFiles.unshift(contextFile); + seenPaths.add(contextFile.path); + } + + if (currentDir === root) { + break; + } + + const parentDir = resolve(currentDir, ".."); + if (parentDir === currentDir) { + break; + } + currentDir = parentDir; + } + + contextFiles.push(...ancestorContextFiles); + + return contextFiles; +} + +export interface DefaultResourceLoaderOptions { + cwd: string; + agentDir: string; + settingsManager?: SettingsManager; + eventBus?: EventBus; + additionalExtensionPaths?: string[]; + additionalSkillPaths?: string[]; + additionalPromptTemplatePaths?: string[]; + additionalThemePaths?: string[]; + extensionFactories?: ExtensionFactory[]; + noExtensions?: boolean; + noSkills?: boolean; + noPromptTemplates?: boolean; + noThemes?: boolean; + noContextFiles?: boolean; + systemPrompt?: string; + appendSystemPrompt?: string[]; + extensionsOverride?: (base: LoadExtensionsResult) => LoadExtensionsResult; + skillsOverride?: (base: { skills: Skill[]; diagnostics: ResourceDiagnostic[] }) => { + skills: Skill[]; + diagnostics: ResourceDiagnostic[]; + }; + promptsOverride?: (base: { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] }) => { + prompts: PromptTemplate[]; + diagnostics: ResourceDiagnostic[]; + }; + themesOverride?: (base: { themes: Theme[]; diagnostics: ResourceDiagnostic[] }) => { + themes: Theme[]; + diagnostics: ResourceDiagnostic[]; + }; + agentsFilesOverride?: (base: { agentsFiles: Array<{ path: string; content: string }> }) => { + agentsFiles: Array<{ path: string; content: string }>; + }; + systemPromptOverride?: (base: string | undefined) => string | undefined; + appendSystemPromptOverride?: (base: string[]) => string[]; +} + +export class DefaultResourceLoader implements ResourceLoader { + private cwd: string; + private agentDir: string; + private settingsManager: SettingsManager; + private eventBus: EventBus; + private packageManager: DefaultPackageManager; + private additionalExtensionPaths: string[]; + private additionalSkillPaths: string[]; + private additionalPromptTemplatePaths: string[]; + private additionalThemePaths: string[]; + private extensionFactories: ExtensionFactory[]; + private noExtensions: boolean; + private noSkills: boolean; + private noPromptTemplates: boolean; + private noThemes: boolean; + private noContextFiles: boolean; + private systemPromptSource?: string; + private appendSystemPromptSource?: string[]; + private extensionsOverride?: (base: LoadExtensionsResult) => LoadExtensionsResult; + private skillsOverride?: (base: { skills: Skill[]; diagnostics: ResourceDiagnostic[] }) => { + skills: Skill[]; + diagnostics: ResourceDiagnostic[]; + }; + private promptsOverride?: (base: { + prompts: PromptTemplate[]; + diagnostics: ResourceDiagnostic[]; + }) => { + prompts: PromptTemplate[]; + diagnostics: ResourceDiagnostic[]; + }; + private themesOverride?: (base: { themes: Theme[]; diagnostics: ResourceDiagnostic[] }) => { + themes: Theme[]; + diagnostics: ResourceDiagnostic[]; + }; + private agentsFilesOverride?: (base: { + agentsFiles: Array<{ path: string; content: string }>; + }) => { + agentsFiles: Array<{ path: string; content: string }>; + }; + private systemPromptOverride?: (base: string | undefined) => string | undefined; + private appendSystemPromptOverride?: (base: string[]) => string[]; + + private extensionsResult: LoadExtensionsResult; + private skills: Skill[]; + private skillDiagnostics: ResourceDiagnostic[]; + private prompts: PromptTemplate[]; + private promptDiagnostics: ResourceDiagnostic[]; + private themes: Theme[]; + private themeDiagnostics: ResourceDiagnostic[]; + private agentsFiles: Array<{ path: string; content: string }>; + private systemPrompt?: string; + private appendSystemPrompt: string[]; + private lastSkillPaths: string[]; + private extensionSkillSourceInfos: Map; + private extensionPromptSourceInfos: Map; + private extensionThemeSourceInfos: Map; + private lastPromptPaths: string[]; + private lastThemePaths: string[]; + + constructor(options: DefaultResourceLoaderOptions) { + this.cwd = options.cwd; + this.agentDir = options.agentDir; + this.settingsManager = + options.settingsManager ?? SettingsManager.create(this.cwd, this.agentDir); + this.eventBus = options.eventBus ?? createEventBus(); + this.packageManager = new DefaultPackageManager({ + cwd: this.cwd, + agentDir: this.agentDir, + settingsManager: this.settingsManager, + }); + this.additionalExtensionPaths = options.additionalExtensionPaths ?? []; + this.additionalSkillPaths = options.additionalSkillPaths ?? []; + this.additionalPromptTemplatePaths = options.additionalPromptTemplatePaths ?? []; + this.additionalThemePaths = options.additionalThemePaths ?? []; + this.extensionFactories = options.extensionFactories ?? []; + this.noExtensions = options.noExtensions ?? false; + this.noSkills = options.noSkills ?? false; + this.noPromptTemplates = options.noPromptTemplates ?? false; + this.noThemes = options.noThemes ?? false; + this.noContextFiles = options.noContextFiles ?? false; + this.systemPromptSource = options.systemPrompt; + this.appendSystemPromptSource = options.appendSystemPrompt; + this.extensionsOverride = options.extensionsOverride; + this.skillsOverride = options.skillsOverride; + this.promptsOverride = options.promptsOverride; + this.themesOverride = options.themesOverride; + this.agentsFilesOverride = options.agentsFilesOverride; + this.systemPromptOverride = options.systemPromptOverride; + this.appendSystemPromptOverride = options.appendSystemPromptOverride; + + this.extensionsResult = { extensions: [], errors: [], runtime: createExtensionRuntime() }; + this.skills = []; + this.skillDiagnostics = []; + this.prompts = []; + this.promptDiagnostics = []; + this.themes = []; + this.themeDiagnostics = []; + this.agentsFiles = []; + this.appendSystemPrompt = []; + this.lastSkillPaths = []; + this.extensionSkillSourceInfos = new Map(); + this.extensionPromptSourceInfos = new Map(); + this.extensionThemeSourceInfos = new Map(); + this.lastPromptPaths = []; + this.lastThemePaths = []; + } + + getExtensions(): LoadExtensionsResult { + return this.extensionsResult; + } + + getSkills(): { skills: Skill[]; diagnostics: ResourceDiagnostic[] } { + return { skills: this.skills, diagnostics: this.skillDiagnostics }; + } + + getPrompts(): { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] } { + return { prompts: this.prompts, diagnostics: this.promptDiagnostics }; + } + + getThemes(): { themes: Theme[]; diagnostics: ResourceDiagnostic[] } { + return { themes: this.themes, diagnostics: this.themeDiagnostics }; + } + + getAgentsFiles(): { agentsFiles: Array<{ path: string; content: string }> } { + return { agentsFiles: this.agentsFiles }; + } + + getSystemPrompt(): string | undefined { + return this.systemPrompt; + } + + getAppendSystemPrompt(): string[] { + return this.appendSystemPrompt; + } + + extendResources(paths: ResourceExtensionPaths): void { + const skillPaths = this.normalizeExtensionPaths(paths.skillPaths ?? []); + const promptPaths = this.normalizeExtensionPaths(paths.promptPaths ?? []); + const themePaths = this.normalizeExtensionPaths(paths.themePaths ?? []); + + for (const entry of skillPaths) { + this.extensionSkillSourceInfos.set(entry.path, createSourceInfo(entry.path, entry.metadata)); + } + for (const entry of promptPaths) { + this.extensionPromptSourceInfos.set(entry.path, createSourceInfo(entry.path, entry.metadata)); + } + for (const entry of themePaths) { + this.extensionThemeSourceInfos.set(entry.path, createSourceInfo(entry.path, entry.metadata)); + } + + if (skillPaths.length > 0) { + this.lastSkillPaths = this.mergePaths( + this.lastSkillPaths, + skillPaths.map((entry) => entry.path), + ); + this.updateSkillsFromPaths(this.lastSkillPaths); + } + + if (promptPaths.length > 0) { + this.lastPromptPaths = this.mergePaths( + this.lastPromptPaths, + promptPaths.map((entry) => entry.path), + ); + this.updatePromptsFromPaths(this.lastPromptPaths); + } + + if (themePaths.length > 0) { + this.lastThemePaths = this.mergePaths( + this.lastThemePaths, + themePaths.map((entry) => entry.path), + ); + this.updateThemesFromPaths(this.lastThemePaths); + } + } + + async reload(): Promise { + await this.settingsManager.reload(); + const resolvedPaths = await this.packageManager.resolve(); + const cliExtensionPaths = await this.packageManager.resolveExtensionSources( + this.additionalExtensionPaths, + { + temporary: true, + }, + ); + const metadataByPath = new Map(); + + this.extensionSkillSourceInfos = new Map(); + this.extensionPromptSourceInfos = new Map(); + this.extensionThemeSourceInfos = new Map(); + + // Helper to extract enabled paths and store metadata + const getEnabledResources = ( + resources: Array<{ path: string; enabled: boolean; metadata: PathMetadata }>, + ): Array<{ path: string; enabled: boolean; metadata: PathMetadata }> => { + for (const r of resources) { + if (!metadataByPath.has(r.path)) { + metadataByPath.set(r.path, r.metadata); + } + } + return resources.filter((r) => r.enabled); + }; + + const getEnabledPaths = ( + resources: Array<{ path: string; enabled: boolean; metadata: PathMetadata }>, + ): string[] => getEnabledResources(resources).map((r) => r.path); + const enabledExtensions = getEnabledPaths(resolvedPaths.extensions); + const enabledSkillResources = getEnabledResources(resolvedPaths.skills); + const enabledPrompts = getEnabledPaths(resolvedPaths.prompts); + const enabledThemes = getEnabledPaths(resolvedPaths.themes); + + const mapSkillPath = (resource: { path: string; metadata: PathMetadata }): string => { + if (resource.metadata.source !== "auto" && resource.metadata.origin !== "package") { + return resource.path; + } + try { + const stats = statSync(resource.path); + if (!stats.isDirectory()) { + return resource.path; + } + } catch { + return resource.path; + } + const skillFile = join(resource.path, "SKILL.md"); + if (existsSync(skillFile)) { + if (!metadataByPath.has(skillFile)) { + metadataByPath.set(skillFile, resource.metadata); + } + return skillFile; + } + return resource.path; + }; + + const enabledSkills = enabledSkillResources.map(mapSkillPath); + + // Add CLI paths metadata + for (const r of cliExtensionPaths.extensions) { + if (!metadataByPath.has(r.path)) { + metadataByPath.set(r.path, { source: "cli", scope: "temporary", origin: "top-level" }); + } + } + for (const r of cliExtensionPaths.skills) { + if (!metadataByPath.has(r.path)) { + metadataByPath.set(r.path, { source: "cli", scope: "temporary", origin: "top-level" }); + } + } + + const cliEnabledExtensions = getEnabledPaths(cliExtensionPaths.extensions); + const cliEnabledSkills = getEnabledPaths(cliExtensionPaths.skills); + const cliEnabledPrompts = getEnabledPaths(cliExtensionPaths.prompts); + const cliEnabledThemes = getEnabledPaths(cliExtensionPaths.themes); + + const extensionPaths = this.noExtensions + ? cliEnabledExtensions + : this.mergePaths(cliEnabledExtensions, enabledExtensions); + + const extensionsResult = await loadExtensions(extensionPaths, this.cwd, this.eventBus); + const inlineExtensions = await this.loadExtensionFactories(extensionsResult.runtime); + extensionsResult.extensions.push(...inlineExtensions.extensions); + extensionsResult.errors.push(...inlineExtensions.errors); + + // Detect extension conflicts (tools, commands, flags with same names from different extensions) + // Keep all extensions loaded. Conflicts are reported as diagnostics, and precedence is handled by load order. + const conflicts = this.detectExtensionConflicts(extensionsResult.extensions); + for (const conflict of conflicts) { + extensionsResult.errors.push({ path: conflict.path, error: conflict.message }); + } + + for (const p of this.additionalExtensionPaths) { + if (isLocalPath(p) && !existsSync(p)) { + extensionsResult.errors.push({ path: p, error: `Extension path does not exist: ${p}` }); + } + } + this.extensionsResult = this.extensionsOverride + ? this.extensionsOverride(extensionsResult) + : extensionsResult; + this.applyExtensionSourceInfo(this.extensionsResult.extensions, metadataByPath); + + const skillPaths = this.noSkills + ? this.mergePaths(cliEnabledSkills, this.additionalSkillPaths) + : this.mergePaths([...cliEnabledSkills, ...enabledSkills], this.additionalSkillPaths); + + this.lastSkillPaths = skillPaths; + this.updateSkillsFromPaths(skillPaths, metadataByPath); + for (const p of this.additionalSkillPaths) { + if (isLocalPath(p) && !existsSync(p) && !this.skillDiagnostics.some((d) => d.path === p)) { + this.skillDiagnostics.push({ + type: "error", + message: "Skill path does not exist", + path: p, + }); + } + } + + const promptPaths = this.noPromptTemplates + ? this.mergePaths(cliEnabledPrompts, this.additionalPromptTemplatePaths) + : this.mergePaths( + [...cliEnabledPrompts, ...enabledPrompts], + this.additionalPromptTemplatePaths, + ); + + this.lastPromptPaths = promptPaths; + this.updatePromptsFromPaths(promptPaths, metadataByPath); + for (const p of this.additionalPromptTemplatePaths) { + if (isLocalPath(p) && !existsSync(p) && !this.promptDiagnostics.some((d) => d.path === p)) { + this.promptDiagnostics.push({ + type: "error", + message: "Prompt template path does not exist", + path: p, + }); + } + } + + const themePaths = this.noThemes + ? this.mergePaths(cliEnabledThemes, this.additionalThemePaths) + : this.mergePaths([...cliEnabledThemes, ...enabledThemes], this.additionalThemePaths); + + this.lastThemePaths = themePaths; + this.updateThemesFromPaths(themePaths, metadataByPath); + for (const p of this.additionalThemePaths) { + if (!existsSync(p) && !this.themeDiagnostics.some((d) => d.path === p)) { + this.themeDiagnostics.push({ + type: "error", + message: "Theme path does not exist", + path: p, + }); + } + } + + const agentsFiles = { + agentsFiles: this.noContextFiles + ? [] + : loadProjectContextFiles({ cwd: this.cwd, agentDir: this.agentDir }), + }; + const resolvedAgentsFiles = this.agentsFilesOverride + ? this.agentsFilesOverride(agentsFiles) + : agentsFiles; + this.agentsFiles = resolvedAgentsFiles.agentsFiles; + + const baseSystemPrompt = resolvePromptInput( + this.systemPromptSource ?? this.discoverSystemPromptFile(), + "system prompt", + ); + this.systemPrompt = this.systemPromptOverride + ? this.systemPromptOverride(baseSystemPrompt) + : baseSystemPrompt; + + const appendSources = + this.appendSystemPromptSource ?? + (this.discoverAppendSystemPromptFile() ? [this.discoverAppendSystemPromptFile()!] : []); + const baseAppend = appendSources + .map((s) => resolvePromptInput(s, "append system prompt")) + .filter((s): s is string => s !== undefined); + this.appendSystemPrompt = this.appendSystemPromptOverride + ? this.appendSystemPromptOverride(baseAppend) + : baseAppend; + } + + private normalizeExtensionPaths( + entries: Array<{ path: string; metadata: PathMetadata }>, + ): Array<{ path: string; metadata: PathMetadata }> { + return entries.map((entry) => ({ + path: this.resolveResourcePath(entry.path), + metadata: entry.metadata, + })); + } + + private updateSkillsFromPaths( + skillPaths: string[], + metadataByPath?: Map, + ): void { + let skillsResult: { skills: Skill[]; diagnostics: ResourceDiagnostic[] }; + if (this.noSkills && skillPaths.length === 0) { + skillsResult = { skills: [], diagnostics: [] }; + } else { + skillsResult = loadSkills({ + cwd: this.cwd, + agentDir: this.agentDir, + skillPaths, + includeDefaults: false, + }); + } + const resolvedSkills = this.skillsOverride ? this.skillsOverride(skillsResult) : skillsResult; + this.skills = resolvedSkills.skills.map((skill) => ({ + ...skill, + sourceInfo: + this.findSourceInfoForPath( + skill.filePath, + this.extensionSkillSourceInfos, + metadataByPath, + ) ?? + skill.sourceInfo ?? + this.getDefaultSourceInfoForPath(skill.filePath), + })); + this.skillDiagnostics = resolvedSkills.diagnostics; + } + + private updatePromptsFromPaths( + promptPaths: string[], + metadataByPath?: Map, + ): void { + let promptsResult: { prompts: PromptTemplate[]; diagnostics: ResourceDiagnostic[] }; + if (this.noPromptTemplates && promptPaths.length === 0) { + promptsResult = { prompts: [], diagnostics: [] }; + } else { + const allPrompts = loadPromptTemplates({ + cwd: this.cwd, + agentDir: this.agentDir, + promptPaths, + includeDefaults: false, + }); + promptsResult = this.dedupePrompts(allPrompts); + } + const resolvedPrompts = this.promptsOverride + ? this.promptsOverride(promptsResult) + : promptsResult; + this.prompts = resolvedPrompts.prompts.map((prompt) => ({ + ...prompt, + sourceInfo: + this.findSourceInfoForPath( + prompt.filePath, + this.extensionPromptSourceInfos, + metadataByPath, + ) ?? + prompt.sourceInfo ?? + this.getDefaultSourceInfoForPath(prompt.filePath), + })); + this.promptDiagnostics = resolvedPrompts.diagnostics; + } + + private updateThemesFromPaths( + themePaths: string[], + metadataByPath?: Map, + ): void { + let themesResult: { themes: Theme[]; diagnostics: ResourceDiagnostic[] }; + if (this.noThemes && themePaths.length === 0) { + themesResult = { themes: [], diagnostics: [] }; + } else { + const loaded = this.loadThemes(themePaths, false); + const deduped = this.dedupeThemes(loaded.themes); + themesResult = { + themes: deduped.themes, + diagnostics: [...loaded.diagnostics, ...deduped.diagnostics], + }; + } + const resolvedThemes = this.themesOverride ? this.themesOverride(themesResult) : themesResult; + this.themes = resolvedThemes.themes.map((theme) => { + const sourcePath = theme.sourcePath; + theme.sourceInfo = sourcePath + ? (this.findSourceInfoForPath(sourcePath, this.extensionThemeSourceInfos, metadataByPath) ?? + theme.sourceInfo ?? + this.getDefaultSourceInfoForPath(sourcePath)) + : theme.sourceInfo; + return theme; + }); + this.themeDiagnostics = resolvedThemes.diagnostics; + } + + private applyExtensionSourceInfo( + extensions: Extension[], + metadataByPath: Map, + ): void { + for (const extension of extensions) { + extension.sourceInfo = + this.findSourceInfoForPath(extension.path, undefined, metadataByPath) ?? + this.getDefaultSourceInfoForPath(extension.path); + for (const command of extension.commands.values()) { + command.sourceInfo = extension.sourceInfo; + } + for (const tool of extension.tools.values()) { + tool.sourceInfo = extension.sourceInfo; + } + } + } + + private findSourceInfoForPath( + resourcePath: string, + extraSourceInfos?: Map, + metadataByPath?: Map, + ): SourceInfo | undefined { + if (!resourcePath) { + return undefined; + } + + if (resourcePath.startsWith("<")) { + return this.getDefaultSourceInfoForPath(resourcePath); + } + + const normalizedResourcePath = resolve(resourcePath); + if (extraSourceInfos) { + for (const [sourcePath, sourceInfo] of extraSourceInfos.entries()) { + const normalizedSourcePath = resolve(sourcePath); + if ( + normalizedResourcePath === normalizedSourcePath || + normalizedResourcePath.startsWith(`${normalizedSourcePath}${sep}`) + ) { + return { ...sourceInfo, path: resourcePath }; + } + } + } + + if (metadataByPath) { + const exact = metadataByPath.get(normalizedResourcePath) ?? metadataByPath.get(resourcePath); + if (exact) { + return createSourceInfo(resourcePath, exact); + } + + for (const [sourcePath, metadata] of metadataByPath.entries()) { + const normalizedSourcePath = resolve(sourcePath); + if ( + normalizedResourcePath === normalizedSourcePath || + normalizedResourcePath.startsWith(`${normalizedSourcePath}${sep}`) + ) { + return createSourceInfo(resourcePath, metadata); + } + } + } + + return undefined; + } + + private getDefaultSourceInfoForPath(filePath: string): SourceInfo { + if (filePath.startsWith("<") && filePath.endsWith(">")) { + return { + path: filePath, + source: filePath.slice(1, -1).split(":")[0] || "temporary", + scope: "temporary", + origin: "top-level", + }; + } + + const normalizedPath = resolve(filePath); + const agentRoots = [ + join(this.agentDir, "skills"), + join(this.agentDir, "prompts"), + join(this.agentDir, "themes"), + join(this.agentDir, "extensions"), + ]; + const projectRoots = [ + join(this.cwd, CONFIG_DIR_NAME, "skills"), + join(this.cwd, CONFIG_DIR_NAME, "prompts"), + join(this.cwd, CONFIG_DIR_NAME, "themes"), + join(this.cwd, CONFIG_DIR_NAME, "extensions"), + ]; + + for (const root of agentRoots) { + if (this.isUnderPath(normalizedPath, root)) { + return { + path: filePath, + source: "local", + scope: "user", + origin: "top-level", + baseDir: root, + }; + } + } + + for (const root of projectRoots) { + if (this.isUnderPath(normalizedPath, root)) { + return { + path: filePath, + source: "local", + scope: "project", + origin: "top-level", + baseDir: root, + }; + } + } + + return { + path: filePath, + source: "local", + scope: "temporary", + origin: "top-level", + baseDir: statSync(normalizedPath).isDirectory() + ? normalizedPath + : resolve(normalizedPath, ".."), + }; + } + + private mergePaths(primary: string[], additional: string[]): string[] { + const merged: string[] = []; + const seen = new Set(); + + for (const p of [...primary, ...additional]) { + const resolved = this.resolveResourcePath(p); + const canonicalPath = canonicalizePath(resolved); + if (seen.has(canonicalPath)) { + continue; + } + seen.add(canonicalPath); + merged.push(resolved); + } + + return merged; + } + + private resolveResourcePath(p: string): string { + const trimmed = p.trim(); + let expanded = trimmed; + if (trimmed === "~") { + expanded = homedir(); + } else if (trimmed.startsWith("~/")) { + expanded = join(homedir(), trimmed.slice(2)); + } else if (trimmed.startsWith("~")) { + expanded = join(homedir(), trimmed.slice(1)); + } + return resolve(this.cwd, expanded); + } + + private loadThemes( + paths: string[], + includeDefaults: boolean = true, + ): { + themes: Theme[]; + diagnostics: ResourceDiagnostic[]; + } { + const themes: Theme[] = []; + const diagnostics: ResourceDiagnostic[] = []; + if (includeDefaults) { + const defaultDirs = [ + join(this.agentDir, "themes"), + join(this.cwd, CONFIG_DIR_NAME, "themes"), + ]; + + for (const dir of defaultDirs) { + this.loadThemesFromDir(dir, themes, diagnostics); + } + } + + for (const p of paths) { + const resolved = resolve(this.cwd, p); + if (!existsSync(resolved)) { + diagnostics.push({ type: "warning", message: "theme path does not exist", path: resolved }); + continue; + } + + try { + const stats = statSync(resolved); + if (stats.isDirectory()) { + this.loadThemesFromDir(resolved, themes, diagnostics); + } else if (stats.isFile() && resolved.endsWith(".json")) { + this.loadThemeFromFile(resolved, themes, diagnostics); + } else { + diagnostics.push({ + type: "warning", + message: "theme path is not a json file", + path: resolved, + }); + } + } catch (error) { + const message = error instanceof Error ? error.message : "failed to read theme path"; + diagnostics.push({ type: "warning", message, path: resolved }); + } + } + + return { themes, diagnostics }; + } + + private loadThemesFromDir(dir: string, themes: Theme[], diagnostics: ResourceDiagnostic[]): void { + if (!existsSync(dir)) { + return; + } + + try { + const entries = readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + let isFile = entry.isFile(); + if (entry.isSymbolicLink()) { + try { + isFile = statSync(join(dir, entry.name)).isFile(); + } catch { + continue; + } + } + if (!isFile) { + continue; + } + if (!entry.name.endsWith(".json")) { + continue; + } + this.loadThemeFromFile(join(dir, entry.name), themes, diagnostics); + } + } catch (error) { + const message = error instanceof Error ? error.message : "failed to read theme directory"; + diagnostics.push({ type: "warning", message, path: dir }); + } + } + + private loadThemeFromFile( + filePath: string, + themes: Theme[], + diagnostics: ResourceDiagnostic[], + ): void { + try { + themes.push(loadThemeFromPath(filePath)); + } catch (error) { + const message = error instanceof Error ? error.message : "failed to load theme"; + diagnostics.push({ type: "warning", message, path: filePath }); + } + } + + private async loadExtensionFactories(runtime: ExtensionRuntime): Promise<{ + extensions: Extension[]; + errors: Array<{ path: string; error: string }>; + }> { + const extensions: Extension[] = []; + const errors: Array<{ path: string; error: string }> = []; + + for (const [index, factory] of this.extensionFactories.entries()) { + const extensionPath = ``; + try { + const extension = await loadExtensionFromFactory( + factory, + this.cwd, + this.eventBus, + runtime, + extensionPath, + ); + extensions.push(extension); + } catch (error) { + const message = error instanceof Error ? error.message : "failed to load extension"; + errors.push({ path: extensionPath, error: message }); + } + } + + return { extensions, errors }; + } + + private dedupePrompts(prompts: PromptTemplate[]): { + prompts: PromptTemplate[]; + diagnostics: ResourceDiagnostic[]; + } { + const seen = new Map(); + const diagnostics: ResourceDiagnostic[] = []; + + for (const prompt of prompts) { + const existing = seen.get(prompt.name); + if (existing) { + diagnostics.push({ + type: "collision", + message: `name "/${prompt.name}" collision`, + path: prompt.filePath, + collision: { + resourceType: "prompt", + name: prompt.name, + winnerPath: existing.filePath, + loserPath: prompt.filePath, + }, + }); + } else { + seen.set(prompt.name, prompt); + } + } + + return { prompts: Array.from(seen.values()), diagnostics }; + } + + private dedupeThemes(themes: Theme[]): { themes: Theme[]; diagnostics: ResourceDiagnostic[] } { + const seen = new Map(); + const diagnostics: ResourceDiagnostic[] = []; + + for (const t of themes) { + const name = t.name ?? "unnamed"; + const existing = seen.get(name); + if (existing) { + diagnostics.push({ + type: "collision", + message: `name "${name}" collision`, + path: t.sourcePath, + collision: { + resourceType: "theme", + name, + winnerPath: existing.sourcePath ?? "", + loserPath: t.sourcePath ?? "", + }, + }); + } else { + seen.set(name, t); + } + } + + return { themes: Array.from(seen.values()), diagnostics }; + } + + private discoverSystemPromptFile(): string | undefined { + const projectPath = join(this.cwd, CONFIG_DIR_NAME, "SYSTEM.md"); + if (existsSync(projectPath)) { + return projectPath; + } + + const globalPath = join(this.agentDir, "SYSTEM.md"); + if (existsSync(globalPath)) { + return globalPath; + } + + return undefined; + } + + private discoverAppendSystemPromptFile(): string | undefined { + const projectPath = join(this.cwd, CONFIG_DIR_NAME, "APPEND_SYSTEM.md"); + if (existsSync(projectPath)) { + return projectPath; + } + + const globalPath = join(this.agentDir, "APPEND_SYSTEM.md"); + if (existsSync(globalPath)) { + return globalPath; + } + + return undefined; + } + + private isUnderPath(target: string, root: string): boolean { + const normalizedRoot = resolve(root); + if (target === normalizedRoot) { + return true; + } + const prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`; + return target.startsWith(prefix); + } + + private detectExtensionConflicts( + extensions: Extension[], + ): Array<{ path: string; message: string }> { + const conflicts: Array<{ path: string; message: string }> = []; + + // Track which extension registered each tool and flag + const toolOwners = new Map(); + const flagOwners = new Map(); + + for (const ext of extensions) { + // Check tools + for (const toolName of ext.tools.keys()) { + const existingOwner = toolOwners.get(toolName); + if (existingOwner && existingOwner !== ext.path) { + conflicts.push({ + path: ext.path, + message: `Tool "${toolName}" conflicts with ${existingOwner}`, + }); + } else { + toolOwners.set(toolName, ext.path); + } + } + + // Check flags + for (const flagName of ext.flags.keys()) { + const existingOwner = flagOwners.get(flagName); + if (existingOwner && existingOwner !== ext.path) { + conflicts.push({ + path: ext.path, + message: `Flag "--${flagName}" conflicts with ${existingOwner}`, + }); + } else { + flagOwners.set(flagName, ext.path); + } + } + } + + return conflicts; + } +} diff --git a/src/agents/sessions/sdk.ts b/src/agents/sessions/sdk.ts new file mode 100644 index 00000000000..bd09fefd396 --- /dev/null +++ b/src/agents/sessions/sdk.ts @@ -0,0 +1,437 @@ +import { join } from "node:path"; +import { + clampThinkingLevel, + type Message, + type Model, + streamSimple, +} from "openclaw/plugin-sdk/llm"; +import { getAgentDir } from "../config.js"; +import { Agent, type AgentMessage, type ThinkingLevel } from "../runtime/index.js"; +import { AgentSession } from "./agent-session.js"; +import { formatNoModelsAvailableMessage } from "./auth-guidance.js"; +import { AuthStorage } from "./auth-storage.js"; +import { DEFAULT_THINKING_LEVEL } from "./defaults.js"; +import type { + ExtensionRunner, + LoadExtensionsResult, + SessionStartEvent, + ToolDefinition, +} from "./extensions/index.js"; +import { convertToLlm } from "./messages.js"; +import { ModelRegistry } from "./model-registry.js"; +import { findInitialModel } from "./model-resolver.js"; +import type { ResourceLoader } from "./resource-loader.js"; +import { DefaultResourceLoader } from "./resource-loader.js"; +import { getDefaultSessionDir, SessionManager } from "./session-manager.js"; +import { SettingsManager } from "./settings-manager.js"; +import { isInstallTelemetryEnabled } from "./telemetry.js"; +import { time } from "./timings.js"; +import { + createBashTool, + createCodingTools, + createEditTool, + createFindTool, + createGrepTool, + createLsTool, + createReadOnlyTools, + createReadTool, + createWriteTool, + type ToolName, + withFileMutationQueue, +} from "./tools/index.js"; + +export interface CreateAgentSessionOptions { + /** Working directory for project-local discovery. Default: process.cwd() */ + cwd?: string; + /** Global config directory. Default: ~/.openclaw/agents/default */ + agentDir?: string; + + /** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */ + authStorage?: AuthStorage; + /** Model registry. Default: ModelRegistry.create(authStorage, agentDir/models.json) */ + modelRegistry?: ModelRegistry; + + /** Model to use. Default: from settings, else first available */ + model?: Model; + /** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */ + thinkingLevel?: ThinkingLevel; + /** Models available for cycling (Ctrl+P in interactive mode) */ + scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>; + + /** + * Optional default tool suppression mode when no explicit allowlist is provided. + * + * - "all": start with no tools enabled + * - "builtin": disable the default built-in tools (read, bash, edit, write) + * but keep extension/custom tools enabled + */ + noTools?: "all" | "builtin"; + /** + * Optional allowlist of tool names. + * + * When omitted, OpenClaw enables the default built-in tools (read, bash, edit, write) + * and leaves extension/custom tools enabled unless `noTools` changes that default. + * When provided, only the listed tool names are enabled. + */ + tools?: string[]; + /** Custom tools to register (in addition to built-in tools). */ + customTools?: ToolDefinition[]; + + /** Resource loader. When omitted, DefaultResourceLoader is used. */ + resourceLoader?: ResourceLoader; + + /** Session manager. Default: SessionManager.create(cwd) */ + sessionManager?: SessionManager; + + /** Settings manager. Default: SettingsManager.create(cwd, agentDir) */ + settingsManager?: SettingsManager; + /** Session start event metadata for extension runtime startup. */ + sessionStartEvent?: SessionStartEvent; +} + +/** Result from createAgentSession */ +export interface CreateAgentSessionResult { + /** The created session */ + session: AgentSession; + /** Extensions result (for UI context setup in interactive mode) */ + extensionsResult: LoadExtensionsResult; + /** Warning if session was restored with a different model than saved */ + modelFallbackMessage?: string; +} + +// Re-exports + +export type { + ExtensionAPI, + ExtensionCommandContext, + ExtensionContext, + ExtensionFactory, + SlashCommandInfo, + SlashCommandSource, + ToolDefinition, +} from "./extensions/index.js"; +export type { PromptTemplate } from "./prompt-templates.js"; +export type { Skill } from "./skills.js"; +export type { Tool } from "./tools/index.js"; + +export { + withFileMutationQueue, + // Tool factories (for custom cwd) + createCodingTools, + createReadOnlyTools, + createReadTool, + createBashTool, + createEditTool, + createWriteTool, + createGrepTool, + createFindTool, + createLsTool, +}; + +// Helper Functions + +function getDefaultAgentDir(): string { + return getAgentDir(); +} + +function getAttributionHeaders( + model: Model, + settingsManager: SettingsManager, +): Record | undefined { + if (!isInstallTelemetryEnabled(settingsManager)) { + return undefined; + } + + if (model.provider === "openrouter" || model.baseUrl.includes("openrouter.ai")) { + return { + "HTTP-Referer": "https://openclaw.ai", + "X-OpenRouter-Title": "OpenClaw", + "X-OpenRouter-Categories": "cli-agent", + }; + } + + if ( + model.provider === "cloudflare-workers-ai" || + model.provider === "cloudflare-ai-gateway" || + model.baseUrl.includes("api.cloudflare.com") || + model.baseUrl.includes("gateway.ai.cloudflare.com") + ) { + return { + "User-Agent": "openclaw", + }; + } + + return undefined; +} + +/** + * Create an AgentSession with the specified options. + * + * @example + * ```typescript + * // Minimal - uses defaults + * const { session } = await createAgentSession(); + * + * // With explicit model + * import { getModel } from 'openclaw/plugin-sdk/llm'; + * const { session } = await createAgentSession({ + * model: getModel('anthropic', 'claude-opus-4-5'), + * thinkingLevel: 'high', + * }); + * + * // Continue previous session + * const { session, modelFallbackMessage } = await createAgentSession({ + * continueSession: true, + * }); + * + * // Full control + * const loader = new DefaultResourceLoader({ + * cwd: process.cwd(), + * agentDir: getAgentDir(), + * settingsManager: SettingsManager.create(), + * }); + * await loader.reload(); + * const { session } = await createAgentSession({ + * model: myModel, + * tools: ["read", "bash"], + * resourceLoader: loader, + * sessionManager: SessionManager.inMemory(), + * }); + * ``` + */ +export async function createAgentSession( + options: CreateAgentSessionOptions = {}, +): Promise { + const cwd = options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd(); + const agentDir = options.agentDir ?? getDefaultAgentDir(); + let resourceLoader = options.resourceLoader; + + // Use provided or create AuthStorage and ModelRegistry + const authPath = options.agentDir ? join(agentDir, "auth.json") : undefined; + const modelsPath = options.agentDir ? join(agentDir, "models.json") : undefined; + const authStorage = options.authStorage ?? AuthStorage.create(authPath); + const modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, modelsPath); + + const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir); + const sessionManager = + options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir)); + + if (!resourceLoader) { + resourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager }); + await resourceLoader.reload(); + time("resourceLoader.reload"); + } + + // Check if session has existing data to restore + const existingSession = sessionManager.buildSessionContext(); + const hasExistingSession = existingSession.messages.length > 0; + const hasThinkingEntry = sessionManager + .getBranch() + .some((entry) => entry.type === "thinking_level_change"); + + let model = options.model; + let modelFallbackMessage: string | undefined; + + // If session has data, try to restore model from it + if (!model && hasExistingSession && existingSession.model) { + const restoredModel = modelRegistry.find( + existingSession.model.provider, + existingSession.model.modelId, + ); + if (restoredModel && modelRegistry.hasConfiguredAuth(restoredModel)) { + model = restoredModel; + } + if (!model) { + modelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId}`; + } + } + + // If still no model, use findInitialModel (checks settings default, then provider defaults) + if (!model) { + const result = await findInitialModel({ + scopedModels: [], + isContinuing: hasExistingSession, + defaultProvider: settingsManager.getDefaultProvider(), + defaultModelId: settingsManager.getDefaultModel(), + defaultThinkingLevel: settingsManager.getDefaultThinkingLevel(), + modelRegistry, + }); + model = result.model; + if (!model) { + modelFallbackMessage = formatNoModelsAvailableMessage(); + } else if (modelFallbackMessage) { + modelFallbackMessage += `. Using ${model.provider}/${model.id}`; + } + } + + let thinkingLevel = options.thinkingLevel; + + // If session has data, restore thinking level from it + if (thinkingLevel === undefined && hasExistingSession) { + thinkingLevel = hasThinkingEntry + ? (existingSession.thinkingLevel as ThinkingLevel) + : (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL); + } + + // Fall back to settings default + if (thinkingLevel === undefined) { + thinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL; + } + + // Clamp to model capabilities + if (!model) { + thinkingLevel = "off"; + } else { + thinkingLevel = clampThinkingLevel(model, thinkingLevel) as ThinkingLevel; + } + + const defaultActiveToolNames: ToolName[] = ["read", "bash", "edit", "write"]; + const allowedToolNames = options.tools ?? (options.noTools === "all" ? [] : undefined); + const initialActiveToolNames: string[] = options.tools + ? [...options.tools] + : options.noTools + ? [] + : defaultActiveToolNames; + + let agent: Agent; + + // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth) + const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => { + const converted = convertToLlm(messages); + // Check setting dynamically so mid-session changes take effect + if (!settingsManager.getBlockImages()) { + return converted; + } + // Filter out ImageContent from all messages, replacing with text placeholder + return converted.map((msg) => { + if (msg.role === "user" || msg.role === "toolResult") { + const content = msg.content; + if (Array.isArray(content)) { + const hasImages = content.some((c) => c.type === "image"); + if (hasImages) { + const filteredContent = content + .map((c) => + c.type === "image" + ? { type: "text" as const, text: "Image reading is disabled." } + : c, + ) + .filter( + (c, i, arr) => + // Dedupe consecutive "Image reading is disabled." texts + !( + c.type === "text" && + c.text === "Image reading is disabled." && + i > 0 && + arr[i - 1].type === "text" && + (arr[i - 1] as { type: "text"; text: string }).text === + "Image reading is disabled." + ), + ); + return Object.assign({}, msg, { content: filteredContent }); + } + } + } + return msg; + }); + }; + + const extensionRunnerRef: { current?: ExtensionRunner } = {}; + + agent = new Agent({ + initialState: { + systemPrompt: "", + model, + thinkingLevel, + tools: [], + }, + convertToLlm: convertToLlmWithBlockImages, + streamFn: async (model, context, options) => { + const auth = await modelRegistry.getApiKeyAndHeaders(model); + if (!auth.ok) { + throw new Error(auth.error); + } + const providerRetrySettings = settingsManager.getProviderRetrySettings(); + const attributionHeaders = getAttributionHeaders(model, settingsManager); + return streamSimple(model, context, { + ...options, + apiKey: auth.apiKey, + timeoutMs: options?.timeoutMs ?? providerRetrySettings.timeoutMs, + maxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries, + maxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs, + headers: + attributionHeaders || auth.headers || options?.headers + ? { ...attributionHeaders, ...auth.headers, ...options?.headers } + : undefined, + }); + }, + onPayload: async (payload, model) => { + void model; + const runner = extensionRunnerRef.current; + if (!runner?.hasHandlers("before_provider_request")) { + return payload; + } + return runner.emitBeforeProviderRequest(payload); + }, + onResponse: async (response, model) => { + void model; + const runner = extensionRunnerRef.current; + if (!runner?.hasHandlers("after_provider_response")) { + return; + } + await runner.emit({ + type: "after_provider_response", + status: response.status, + headers: response.headers, + }); + }, + sessionId: sessionManager.getSessionId(), + transformContext: async (messages) => { + const runner = extensionRunnerRef.current; + if (!runner) { + return messages; + } + return runner.emitContext(messages); + }, + steeringMode: settingsManager.getSteeringMode(), + followUpMode: settingsManager.getFollowUpMode(), + transport: settingsManager.getTransport(), + thinkingBudgets: settingsManager.getThinkingBudgets(), + maxRetryDelayMs: settingsManager.getProviderRetrySettings().maxRetryDelayMs, + }); + + // Restore messages if session has existing data + if (hasExistingSession) { + agent.state.messages = existingSession.messages; + if (!hasThinkingEntry) { + sessionManager.appendThinkingLevelChange(thinkingLevel); + } + } else { + // Save initial model and thinking level for new sessions so they can be restored on resume + if (model) { + sessionManager.appendModelChange(model.provider, model.id); + } + sessionManager.appendThinkingLevelChange(thinkingLevel); + } + + const session = new AgentSession({ + agent, + sessionManager, + settingsManager, + cwd, + scopedModels: options.scopedModels, + resourceLoader, + customTools: options.customTools, + modelRegistry, + initialActiveToolNames, + allowedToolNames, + extensionRunnerRef, + sessionStartEvent: options.sessionStartEvent, + }); + const extensionsResult = resourceLoader.getExtensions(); + + return { + session, + extensionsResult, + modelFallbackMessage, + }; +} diff --git a/src/agents/sessions/session-cwd.ts b/src/agents/sessions/session-cwd.ts new file mode 100644 index 00000000000..92ac5aba2db --- /dev/null +++ b/src/agents/sessions/session-cwd.ts @@ -0,0 +1,62 @@ +import { existsSync } from "node:fs"; + +export interface SessionCwdIssue { + sessionFile?: string; + sessionCwd: string; + fallbackCwd: string; +} + +interface SessionCwdSource { + getCwd(): string; + getSessionFile(): string | undefined; +} + +export function getMissingSessionCwdIssue( + sessionManager: SessionCwdSource, + fallbackCwd: string, +): SessionCwdIssue | undefined { + const sessionFile = sessionManager.getSessionFile(); + if (!sessionFile) { + return undefined; + } + + const sessionCwd = sessionManager.getCwd(); + if (!sessionCwd || existsSync(sessionCwd)) { + return undefined; + } + + return { + sessionFile, + sessionCwd, + fallbackCwd, + }; +} + +export function formatMissingSessionCwdError(issue: SessionCwdIssue): string { + const sessionFile = issue.sessionFile ? `\nSession file: ${issue.sessionFile}` : ""; + return `Stored session working directory does not exist: ${issue.sessionCwd}${sessionFile}\nCurrent working directory: ${issue.fallbackCwd}`; +} + +export function formatMissingSessionCwdPrompt(issue: SessionCwdIssue): string { + return `cwd from session file does not exist\n${issue.sessionCwd}\n\ncontinue in current cwd\n${issue.fallbackCwd}`; +} + +export class MissingSessionCwdError extends Error { + readonly issue: SessionCwdIssue; + + constructor(issue: SessionCwdIssue) { + super(formatMissingSessionCwdError(issue)); + this.name = "MissingSessionCwdError"; + this.issue = issue; + } +} + +export function assertSessionCwdExists( + sessionManager: SessionCwdSource, + fallbackCwd: string, +): void { + const issue = getMissingSessionCwdIssue(sessionManager, fallbackCwd); + if (issue) { + throw new MissingSessionCwdError(issue); + } +} diff --git a/src/agents/sessions/session-manager.ts b/src/agents/sessions/session-manager.ts new file mode 100644 index 00000000000..ba88569cfa7 --- /dev/null +++ b/src/agents/sessions/session-manager.ts @@ -0,0 +1,1541 @@ +import { randomUUID } from "node:crypto"; +import { + appendFileSync, + closeSync, + existsSync, + mkdirSync, + openSync, + readdirSync, + readFileSync, + readSync, + statSync, + writeFileSync, +} from "node:fs"; +import { readdir, readFile, stat } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import type { ImageContent, Message, TextContent } from "openclaw/plugin-sdk/llm"; +import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js"; +import { type AgentMessage, uuidv7 } from "../runtime/index.js"; +import { + type BashExecutionMessage, + type CustomMessage, + createBranchSummaryMessage, + createCompactionSummaryMessage, + createCustomMessage, +} from "./messages.js"; + +export const CURRENT_SESSION_VERSION = 3; + +export interface SessionHeader { + type: "session"; + version?: number; // v1 sessions don't have this + id: string; + timestamp: string; + cwd: string; + parentSession?: string; +} + +export interface NewSessionOptions { + id?: string; + parentSession?: string; +} + +export interface SessionEntryBase { + type: string; + id: string; + parentId: string | null; + timestamp: string; +} + +export interface SessionMessageEntry extends SessionEntryBase { + type: "message"; + message: AgentMessage; +} + +export interface ThinkingLevelChangeEntry extends SessionEntryBase { + type: "thinking_level_change"; + thinkingLevel: string; +} + +export interface ModelChangeEntry extends SessionEntryBase { + type: "model_change"; + provider: string; + modelId: string; +} + +export interface CompactionEntry extends SessionEntryBase { + type: "compaction"; + summary: string; + firstKeptEntryId: string; + tokensBefore: number; + /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */ + details?: T; + /** True if generated by an extension, undefined/false if runtime-generated (backward compatible) */ + fromHook?: boolean; +} + +export interface BranchSummaryEntry extends SessionEntryBase { + type: "branch_summary"; + fromId: string; + summary: string; + /** Extension-specific data (not sent to LLM) */ + details?: T; + /** True if generated by an extension, false if runtime-generated */ + fromHook?: boolean; +} + +/** + * Custom entry for extensions to store extension-specific data in the session. + * Use customType to identify your extension's entries. + * + * Purpose: Persist extension state across session reloads. On reload, extensions can + * scan entries for their customType and reconstruct internal state. + * + * Does NOT participate in LLM context (ignored by buildSessionContext). + * For injecting content into context, see CustomMessageEntry. + */ +export interface CustomEntry extends SessionEntryBase { + type: "custom"; + customType: string; + data?: T; +} + +/** Label entry for user-defined bookmarks/markers on entries. */ +export interface LabelEntry extends SessionEntryBase { + type: "label"; + targetId: string; + label: string | undefined; +} + +/** Session metadata entry (e.g., user-defined display name). */ +export interface SessionInfoEntry extends SessionEntryBase { + type: "session_info"; + name?: string; +} + +/** + * Custom message entry for extensions to inject messages into LLM context. + * Use customType to identify your extension's entries. + * + * Unlike CustomEntry, this DOES participate in LLM context. + * The content is converted to a user message in buildSessionContext(). + * Use details for extension-specific metadata (not sent to LLM). + * + * display controls TUI rendering: + * - false: hidden entirely + * - true: rendered with distinct styling (different from user messages) + */ +export interface CustomMessageEntry extends SessionEntryBase { + type: "custom_message"; + customType: string; + content: string | (TextContent | ImageContent)[]; + details?: T; + display: boolean; +} + +/** Session entry - has id/parentId for tree structure (returned by "read" methods in SessionManager) */ +export type SessionEntry = + | SessionMessageEntry + | ThinkingLevelChangeEntry + | ModelChangeEntry + | CompactionEntry + | BranchSummaryEntry + | CustomEntry + | CustomMessageEntry + | LabelEntry + | SessionInfoEntry; + +/** Raw file entry (includes header) */ +export type FileEntry = SessionHeader | SessionEntry; + +/** Tree node for getTree() - defensive copy of session structure */ +export interface SessionTreeNode { + entry: SessionEntry; + children: SessionTreeNode[]; + /** Resolved label for this entry, if any */ + label?: string; + /** Timestamp of the latest label change for this entry, if any */ + labelTimestamp?: string; +} + +export interface SessionContext { + messages: AgentMessage[]; + thinkingLevel: string; + model: { provider: string; modelId: string } | null; +} + +export interface SessionInfo { + path: string; + id: string; + /** Working directory where the session was started. Empty string for old sessions. */ + cwd: string; + /** User-defined display name from session_info entries. */ + name?: string; + /** Path to the parent session (if this session was forked). */ + parentSessionPath?: string; + created: Date; + modified: Date; + messageCount: number; + firstMessage: string; + allMessagesText: string; +} + +export type ReadonlySessionManager = Pick< + SessionManager, + | "getCwd" + | "getSessionDir" + | "getSessionId" + | "getSessionFile" + | "getLeafId" + | "getLeafEntry" + | "getEntry" + | "getLabel" + | "getBranch" + | "getHeader" + | "getEntries" + | "getTree" + | "getSessionName" +>; + +function createSessionId(): string { + return uuidv7(); +} + +/** Generate a unique short ID (8 hex chars, collision-checked) */ +function generateId(byId: { has(id: string): boolean }): string { + for (let i = 0; i < 100; i++) { + const id = randomUUID().slice(0, 8); + if (!byId.has(id)) { + return id; + } + } + // Fallback to full UUID if somehow we have collisions + return randomUUID(); +} + +/** Migrate v1 → v2: add id/parentId tree structure. Mutates in place. */ +function migrateV1ToV2(entries: FileEntry[]): void { + const ids = new Set(); + let prevId: string | null = null; + + for (const entry of entries) { + if (entry.type === "session") { + entry.version = 2; + continue; + } + + entry.id = generateId(ids); + entry.parentId = prevId; + prevId = entry.id; + + // Convert firstKeptEntryIndex to firstKeptEntryId for compaction + if (entry.type === "compaction") { + const comp = entry as CompactionEntry & { firstKeptEntryIndex?: number }; + if (typeof comp.firstKeptEntryIndex === "number") { + const targetEntry = entries[comp.firstKeptEntryIndex]; + if (targetEntry && targetEntry.type !== "session") { + comp.firstKeptEntryId = targetEntry.id; + } + delete comp.firstKeptEntryIndex; + } + } + } +} + +/** Migrate v2 → v3: rename hookMessage role to custom. Mutates in place. */ +function migrateV2ToV3(entries: FileEntry[]): void { + for (const entry of entries) { + if (entry.type === "session") { + entry.version = 3; + continue; + } + + // Update message entries with hookMessage role + if (entry.type === "message") { + const msgEntry = entry; + if (msgEntry.message && (msgEntry.message as { role: string }).role === "hookMessage") { + (msgEntry.message as { role: string }).role = "custom"; + } + } + } +} + +/** + * Run all necessary migrations to bring entries to current version. + * Mutates entries in place. Returns true if any migration was applied. + */ +function migrateToCurrentVersion(entries: FileEntry[]): boolean { + const header = entries.find((e) => e.type === "session"); + const version = header?.version ?? 1; + + if (version >= CURRENT_SESSION_VERSION) { + return false; + } + + if (version < 2) { + migrateV1ToV2(entries); + } + if (version < 3) { + migrateV2ToV3(entries); + } + + return true; +} + +/** Exported for testing */ +export function migrateSessionEntries(entries: FileEntry[]): void { + migrateToCurrentVersion(entries); +} + +/** Exported for compaction.test.ts */ +export function parseSessionEntries(content: string): FileEntry[] { + const entries: FileEntry[] = []; + const lines = content.trim().split("\n"); + + for (const line of lines) { + if (!line.trim()) { + continue; + } + try { + const entry = JSON.parse(line) as FileEntry; + entries.push(entry); + } catch { + // Skip malformed lines + } + } + + return entries; +} + +export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null { + for (let i = entries.length - 1; i >= 0; i--) { + if (entries[i].type === "compaction") { + return entries[i] as CompactionEntry; + } + } + return null; +} + +/** + * Build the session context from entries using tree traversal. + * If leafId is provided, walks from that entry to root. + * Handles compaction and branch summaries along the path. + */ +export function buildSessionContext( + entries: SessionEntry[], + leafId?: string | null, + byId?: Map, +): SessionContext { + // Build uuid index if not available + if (!byId) { + byId = new Map(); + for (const entry of entries) { + byId.set(entry.id, entry); + } + } + + // Find leaf + let leaf: SessionEntry | undefined; + if (leafId === null) { + // Explicitly null - return no messages (navigated to before first entry) + return { messages: [], thinkingLevel: "off", model: null }; + } + if (leafId) { + leaf = byId.get(leafId); + } + if (!leaf) { + // Fallback to last entry (when leafId is undefined) + leaf = entries[entries.length - 1]; + } + + if (!leaf) { + return { messages: [], thinkingLevel: "off", model: null }; + } + + // Walk from leaf to root, collecting path + const path: SessionEntry[] = []; + let current: SessionEntry | undefined = leaf; + while (current) { + path.unshift(current); + current = current.parentId ? byId.get(current.parentId) : undefined; + } + + // Extract settings and find compaction + let thinkingLevel = "off"; + let model: { provider: string; modelId: string } | null = null; + let compaction: CompactionEntry | null = null; + + for (const entry of path) { + if (entry.type === "thinking_level_change") { + thinkingLevel = entry.thinkingLevel; + } else if (entry.type === "model_change") { + model = { provider: entry.provider, modelId: entry.modelId }; + } else if (entry.type === "message" && entry.message.role === "assistant") { + model = { provider: entry.message.provider, modelId: entry.message.model }; + } else if (entry.type === "compaction") { + compaction = entry; + } + } + + // Build messages and collect corresponding entries + // When there's a compaction, we need to: + // 1. Emit summary first (entry = compaction) + // 2. Emit kept messages (from firstKeptEntryId up to compaction) + // 3. Emit messages after compaction + const messages: AgentMessage[] = []; + + const appendMessage = (entry: SessionEntry) => { + if (entry.type === "message") { + messages.push(entry.message); + } else if (entry.type === "custom_message") { + messages.push( + createCustomMessage( + entry.customType, + entry.content, + entry.display, + entry.details, + entry.timestamp, + ), + ); + } else if (entry.type === "branch_summary" && entry.summary) { + messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp)); + } + }; + + if (compaction) { + // Emit summary first + messages.push( + createCompactionSummaryMessage( + compaction.summary, + compaction.tokensBefore, + compaction.timestamp, + ), + ); + + // Find compaction index in path + const compactionIdx = path.findIndex((e) => e.type === "compaction" && e.id === compaction.id); + + // Emit kept messages (before compaction, starting from firstKeptEntryId) + let foundFirstKept = false; + for (let i = 0; i < compactionIdx; i++) { + const entry = path[i]; + if (entry.id === compaction.firstKeptEntryId) { + foundFirstKept = true; + } + if (foundFirstKept) { + appendMessage(entry); + } + } + + // Emit messages after compaction + for (let i = compactionIdx + 1; i < path.length; i++) { + const entry = path[i]; + appendMessage(entry); + } + } else { + // No compaction - emit all messages, handle branch summaries and custom messages + for (const entry of path) { + appendMessage(entry); + } + } + + return { messages, thinkingLevel, model }; +} + +/** + * Compute the default session directory for a cwd. + * Encodes cwd into a safe directory name under ~/.openclaw/agent/sessions/. + */ +export function getDefaultSessionDir(cwd: string, agentDir: string = getDefaultAgentDir()): string { + const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`; + const sessionDir = join(agentDir, "sessions", safePath); + if (!existsSync(sessionDir)) { + mkdirSync(sessionDir, { recursive: true }); + } + return sessionDir; +} + +/** Exported for testing */ +export function loadEntriesFromFile(filePath: string): FileEntry[] { + if (!existsSync(filePath)) { + return []; + } + + const content = readFileSync(filePath, "utf8"); + const entries: FileEntry[] = []; + const lines = content.trim().split("\n"); + + for (const line of lines) { + if (!line.trim()) { + continue; + } + try { + const entry = JSON.parse(line) as FileEntry; + entries.push(entry); + } catch { + // Skip malformed lines + } + } + + // Validate session header + if (entries.length === 0) { + return entries; + } + const header = entries[0]; + if (header.type !== "session" || typeof (header as { id?: unknown }).id !== "string") { + return []; + } + + return entries; +} + +function isValidSessionFile(filePath: string): boolean { + try { + const fd = openSync(filePath, "r"); + const buffer = Buffer.alloc(512); + const bytesRead = readSync(fd, buffer, 0, 512, 0); + closeSync(fd); + const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n")[0]; + if (!firstLine) { + return false; + } + const header = JSON.parse(firstLine); + return header.type === "session" && typeof header.id === "string"; + } catch { + return false; + } +} + +/** Exported for testing */ +export function findMostRecentSession(sessionDir: string): string | null { + try { + const files = readdirSync(sessionDir) + .filter((f) => f.endsWith(".jsonl")) + .map((f) => join(sessionDir, f)) + .filter(isValidSessionFile) + .map((path) => ({ path, mtime: statSync(path).mtime })) + .toSorted((a, b) => b.mtime.getTime() - a.mtime.getTime()); + + return files[0]?.path || null; + } catch { + return null; + } +} + +function isMessageWithContent(message: AgentMessage): message is Message { + return typeof (message as Message).role === "string" && "content" in message; +} + +function extractTextContent(message: Message): string { + const content = message.content; + if (typeof content === "string") { + return content; + } + return content + .filter((block): block is TextContent => block.type === "text") + .map((block) => block.text) + .join(" "); +} + +function getLastActivityTime(entries: FileEntry[]): number | undefined { + let lastActivityTime: number | undefined; + + for (const entry of entries) { + if (entry.type !== "message") { + continue; + } + + const message = entry.message; + if (!isMessageWithContent(message)) { + continue; + } + if (message.role !== "user" && message.role !== "assistant") { + continue; + } + + const msgTimestamp = (message as { timestamp?: number }).timestamp; + if (typeof msgTimestamp === "number") { + lastActivityTime = Math.max(lastActivityTime ?? 0, msgTimestamp); + continue; + } + + const entryTimestamp = (entry as SessionEntryBase).timestamp; + if (typeof entryTimestamp === "string") { + const t = new Date(entryTimestamp).getTime(); + if (!Number.isNaN(t)) { + lastActivityTime = Math.max(lastActivityTime ?? 0, t); + } + } + } + + return lastActivityTime; +} + +function getSessionModifiedDate( + entries: FileEntry[], + header: SessionHeader, + statsMtime: Date, +): Date { + const lastActivityTime = getLastActivityTime(entries); + if (typeof lastActivityTime === "number" && lastActivityTime > 0) { + return new Date(lastActivityTime); + } + + const headerTime = + typeof header.timestamp === "string" ? new Date(header.timestamp).getTime() : Number.NaN; + return !Number.isNaN(headerTime) ? new Date(headerTime) : statsMtime; +} + +async function buildSessionInfo(filePath: string): Promise { + try { + const content = await readFile(filePath, "utf8"); + const entries: FileEntry[] = []; + const lines = content.trim().split("\n"); + + for (const line of lines) { + if (!line.trim()) { + continue; + } + try { + entries.push(JSON.parse(line) as FileEntry); + } catch { + // Skip malformed lines + } + } + + if (entries.length === 0) { + return null; + } + const header = entries[0]; + if (header.type !== "session") { + return null; + } + + const stats = await stat(filePath); + let messageCount = 0; + let firstMessage = ""; + const allMessages: string[] = []; + let name: string | undefined; + + for (const entry of entries) { + // Extract session name (use latest, including explicit clears) + if (entry.type === "session_info") { + const infoEntry = entry; + name = infoEntry.name?.trim() || undefined; + } + + if (entry.type !== "message") { + continue; + } + messageCount++; + + const message = entry.message; + if (!isMessageWithContent(message)) { + continue; + } + if (message.role !== "user" && message.role !== "assistant") { + continue; + } + + const textContent = extractTextContent(message); + if (!textContent) { + continue; + } + + allMessages.push(textContent); + if (!firstMessage && message.role === "user") { + firstMessage = textContent; + } + } + + const cwd = typeof header.cwd === "string" ? header.cwd : ""; + const parentSessionPath = header.parentSession; + + const modified = getSessionModifiedDate(entries, header, stats.mtime); + + return { + path: filePath, + id: header.id, + cwd, + name, + parentSessionPath, + created: new Date(header.timestamp), + modified, + messageCount, + firstMessage: firstMessage || "(no messages)", + allMessagesText: allMessages.join(" "), + }; + } catch { + return null; + } +} + +export type SessionListProgress = (loaded: number, total: number) => void; + +const MAX_CONCURRENT_SESSION_INFO_LOADS = 10; + +async function buildSessionInfosWithConcurrency( + files: string[], + onLoaded: () => void, +): Promise<(SessionInfo | null)[]> { + const results: (SessionInfo | null)[] = Array.from({ length: files.length }, () => null); + const inFlight = new Set>(); + let nextIndex = 0; + + const startNext = (): void => { + const index = nextIndex++; + const file = files[index]; + if (!file) { + return; + } + + let task: Promise; + task = buildSessionInfo(file) + .then((info) => { + results[index] = info; + }) + .catch(() => { + results[index] = null; + }) + .finally(() => { + inFlight.delete(task); + onLoaded(); + }); + inFlight.add(task); + }; + + while (nextIndex < files.length || inFlight.size > 0) { + while (nextIndex < files.length && inFlight.size < MAX_CONCURRENT_SESSION_INFO_LOADS) { + startNext(); + } + if (inFlight.size > 0) { + await Promise.race(inFlight); + } + } + + return results; +} + +async function listSessionsFromDir( + dir: string, + onProgress?: SessionListProgress, + progressOffset = 0, + progressTotal?: number, +): Promise { + const sessions: SessionInfo[] = []; + if (!existsSync(dir)) { + return sessions; + } + + try { + const dirEntries = await readdir(dir); + const files = dirEntries.filter((f) => f.endsWith(".jsonl")).map((f) => join(dir, f)); + const total = progressTotal ?? files.length; + + let loaded = 0; + const results = await buildSessionInfosWithConcurrency(files, () => { + loaded++; + onProgress?.(progressOffset + loaded, total); + }); + for (const info of results) { + if (info) { + sessions.push(info); + } + } + } catch { + // Return empty list on error + } + + return sessions; +} + +/** + * Manages conversation sessions as append-only trees stored in JSONL files. + * + * Each session entry has an id and parentId forming a tree structure. The "leaf" + * pointer tracks the current position. Appending creates a child of the current leaf. + * Branching moves the leaf to an earlier entry, allowing new branches without + * modifying history. + * + * Use buildSessionContext() to get the resolved message list for the LLM, which + * handles compaction summaries and follows the path from root to current leaf. + */ +export class SessionManager { + private sessionId: string = ""; + private sessionFile: string | undefined; + private sessionDir: string; + private cwd: string; + private shouldPersist: boolean; + private flushed: boolean = false; + private fileEntries: FileEntry[] = []; + private byId: Map = new Map(); + private labelsById: Map = new Map(); + private labelTimestampsById: Map = new Map(); + private leafId: string | null = null; + + private constructor( + cwd: string, + sessionDir: string, + sessionFile: string | undefined, + persist: boolean, + ) { + this.cwd = cwd; + this.sessionDir = sessionDir; + this.shouldPersist = persist; + if (persist && sessionDir && !existsSync(sessionDir)) { + mkdirSync(sessionDir, { recursive: true }); + } + + if (sessionFile) { + this.setSessionFile(sessionFile); + } else { + this.newSession(); + } + } + + /** Switch to a different session file (used for resume and branching) */ + setSessionFile(sessionFile: string): void { + this.sessionFile = resolve(sessionFile); + if (existsSync(this.sessionFile)) { + this.fileEntries = loadEntriesFromFile(this.sessionFile); + + // If file was empty or corrupted (no valid header), truncate and start fresh + // to avoid appending messages without a session header (which breaks the session) + if (this.fileEntries.length === 0) { + const explicitPath = this.sessionFile; + this.newSession(); + this.sessionFile = explicitPath; + this.rewriteFile(); + this.flushed = true; + return; + } + + const header = this.fileEntries.find((e) => e.type === "session"); + this.sessionId = header?.id ?? createSessionId(); + + if (migrateToCurrentVersion(this.fileEntries)) { + this.rewriteFile(); + } + + this.buildIndex(); + this.flushed = true; + } else { + const explicitPath = this.sessionFile; + this.newSession(); + this.sessionFile = explicitPath; // preserve explicit path from --session flag + } + } + + newSession(options?: NewSessionOptions): string | undefined { + this.sessionId = options?.id ?? createSessionId(); + const timestamp = new Date().toISOString(); + const header: SessionHeader = { + type: "session", + version: CURRENT_SESSION_VERSION, + id: this.sessionId, + timestamp, + cwd: this.cwd, + parentSession: options?.parentSession, + }; + this.fileEntries = [header]; + this.byId.clear(); + this.labelsById.clear(); + this.leafId = null; + this.flushed = false; + + if (this.shouldPersist) { + const fileTimestamp = timestamp.replace(/[:.]/g, "-"); + this.sessionFile = join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`); + } + return this.sessionFile; + } + + private buildIndex(): void { + this.byId.clear(); + this.labelsById.clear(); + this.labelTimestampsById.clear(); + this.leafId = null; + for (const entry of this.fileEntries) { + if (entry.type === "session") { + continue; + } + this.byId.set(entry.id, entry); + this.leafId = entry.id; + if (entry.type === "label") { + if (entry.label) { + this.labelsById.set(entry.targetId, entry.label); + this.labelTimestampsById.set(entry.targetId, entry.timestamp); + } else { + this.labelsById.delete(entry.targetId); + this.labelTimestampsById.delete(entry.targetId); + } + } + } + } + + private rewriteFile(): void { + if (!this.shouldPersist || !this.sessionFile) { + return; + } + const content = `${this.fileEntries.map((e) => JSON.stringify(e)).join("\n")}\n`; + writeFileSync(this.sessionFile, content); + } + + isPersisted(): boolean { + return this.shouldPersist; + } + + getCwd(): string { + return this.cwd; + } + + getSessionDir(): string { + return this.sessionDir; + } + + getSessionId(): string { + return this.sessionId; + } + + getSessionFile(): string | undefined { + return this.sessionFile; + } + + persist(entry: SessionEntry): void { + if (!this.shouldPersist || !this.sessionFile) { + return; + } + + const hasAssistant = this.fileEntries.some( + (e) => e.type === "message" && e.message.role === "assistant", + ); + if (!hasAssistant) { + // Mark as not flushed so when assistant arrives, all entries get written + this.flushed = false; + return; + } + + if (!this.flushed) { + for (const e of this.fileEntries) { + appendFileSync(this.sessionFile, `${JSON.stringify(e)}\n`); + } + this.flushed = true; + } else { + appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`); + } + } + + private appendEntry(entry: SessionEntry): void { + this.fileEntries.push(entry); + this.byId.set(entry.id, entry); + this.leafId = entry.id; + this.persist(entry); + } + + /** Append a message as child of current leaf, then advance leaf. Returns entry id. + * Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly. + * Reason: we want these to be top-level entries in the session, not message session entries, + * so it is easier to find them. + * These need to be appended via appendCompaction() and appendBranchSummary() methods. + */ + appendMessage(message: Message | CustomMessage | BashExecutionMessage): string { + const entry: SessionMessageEntry = { + type: "message", + id: generateId(this.byId), + parentId: this.leafId, + timestamp: new Date().toISOString(), + message, + }; + this.appendEntry(entry); + return entry.id; + } + + /** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */ + appendThinkingLevelChange(thinkingLevel: string): string { + const entry: ThinkingLevelChangeEntry = { + type: "thinking_level_change", + id: generateId(this.byId), + parentId: this.leafId, + timestamp: new Date().toISOString(), + thinkingLevel, + }; + this.appendEntry(entry); + return entry.id; + } + + /** Append a model change as child of current leaf, then advance leaf. Returns entry id. */ + appendModelChange(provider: string, modelId: string): string { + const entry: ModelChangeEntry = { + type: "model_change", + id: generateId(this.byId), + parentId: this.leafId, + timestamp: new Date().toISOString(), + provider, + modelId, + }; + this.appendEntry(entry); + return entry.id; + } + + /** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */ + appendCompaction( + summary: string, + firstKeptEntryId: string, + tokensBefore: number, + details?: unknown, + fromHook?: boolean, + ): string { + const entry: CompactionEntry = { + type: "compaction", + id: generateId(this.byId), + parentId: this.leafId, + timestamp: new Date().toISOString(), + summary, + firstKeptEntryId, + tokensBefore, + details, + fromHook, + }; + this.appendEntry(entry); + return entry.id; + } + + /** Append a custom entry (for extensions) as child of current leaf, then advance leaf. Returns entry id. */ + appendCustomEntry(customType: string, data?: unknown): string { + const entry: CustomEntry = { + type: "custom", + customType, + data, + id: generateId(this.byId), + parentId: this.leafId, + timestamp: new Date().toISOString(), + }; + this.appendEntry(entry); + return entry.id; + } + + /** Append a session info entry (e.g., display name). Returns entry id. */ + appendSessionInfo(name: string): string { + const entry: SessionInfoEntry = { + type: "session_info", + id: generateId(this.byId), + parentId: this.leafId, + timestamp: new Date().toISOString(), + name: name.trim(), + }; + this.appendEntry(entry); + return entry.id; + } + + /** Get the current session name from the latest session_info entry, if any. */ + getSessionName(): string | undefined { + // Walk entries in reverse to find the latest session_info entry. + // Empty names explicitly clear the session title. + const entries = this.getEntries(); + for (let i = entries.length - 1; i >= 0; i--) { + const entry = entries[i]; + if (entry.type === "session_info") { + return entry.name?.trim() || undefined; + } + } + return undefined; + } + + /** + * Append a custom message entry (for extensions) that participates in LLM context. + * @param customType Extension identifier for filtering on reload + * @param content Message content (string or TextContent/ImageContent array) + * @param display Whether to show in TUI (true = styled display, false = hidden) + * @param details Optional extension-specific metadata (not sent to LLM) + * @returns Entry id + */ + appendCustomMessageEntry( + customType: string, + content: string | (TextContent | ImageContent)[], + display: boolean, + details?: unknown, + ): string { + const entry: CustomMessageEntry = { + type: "custom_message", + customType, + content, + display, + details, + id: generateId(this.byId), + parentId: this.leafId, + timestamp: new Date().toISOString(), + }; + this.appendEntry(entry); + return entry.id; + } + + // ========================================================================= + // Tree Traversal + // ========================================================================= + + getLeafId(): string | null { + return this.leafId; + } + + getLeafEntry(): SessionEntry | undefined { + return this.leafId ? this.byId.get(this.leafId) : undefined; + } + + getEntry(id: string): SessionEntry | undefined { + return this.byId.get(id); + } + + /** + * Get all direct children of an entry. + */ + getChildren(parentId: string): SessionEntry[] { + const children: SessionEntry[] = []; + for (const entry of this.byId.values()) { + if (entry.parentId === parentId) { + children.push(entry); + } + } + return children; + } + + /** + * Get the label for an entry, if any. + */ + getLabel(id: string): string | undefined { + return this.labelsById.get(id); + } + + /** + * Set or clear a label on an entry. + * Labels are user-defined markers for bookmarking/navigation. + * Pass undefined or empty string to clear the label. + */ + appendLabelChange(targetId: string, label: string | undefined): string { + if (!this.byId.has(targetId)) { + throw new Error(`Entry ${targetId} not found`); + } + const entry: LabelEntry = { + type: "label", + id: generateId(this.byId), + parentId: this.leafId, + timestamp: new Date().toISOString(), + targetId, + label, + }; + this.appendEntry(entry); + if (label) { + this.labelsById.set(targetId, label); + this.labelTimestampsById.set(targetId, entry.timestamp); + } else { + this.labelsById.delete(targetId); + this.labelTimestampsById.delete(targetId); + } + return entry.id; + } + + /** + * Walk from entry to root, returning all entries in path order. + * Includes all entry types (messages, compaction, model changes, etc.). + * Use buildSessionContext() to get the resolved messages for the LLM. + */ + getBranch(fromId?: string): SessionEntry[] { + const path: SessionEntry[] = []; + const startId = fromId ?? this.leafId; + let current = startId ? this.byId.get(startId) : undefined; + while (current) { + path.unshift(current); + current = current.parentId ? this.byId.get(current.parentId) : undefined; + } + return path; + } + + /** + * Build the session context (what gets sent to the LLM). + * Uses tree traversal from current leaf. + */ + buildSessionContext(): SessionContext { + return buildSessionContext(this.getEntries(), this.leafId, this.byId); + } + + /** + * Get session header. + */ + getHeader(): SessionHeader | null { + const h = this.fileEntries.find((e) => e.type === "session"); + return h ? h : null; + } + + /** + * Get all session entries (excludes header). Returns a shallow copy. + * The session is append-only: use appendXXX() to add entries, branch() to + * change the leaf pointer. Entries cannot be modified or deleted. + */ + getEntries(): SessionEntry[] { + return this.fileEntries.filter((e): e is SessionEntry => e.type !== "session"); + } + + /** + * Get the session as a tree structure. Returns a shallow defensive copy of all entries. + * A well-formed session has exactly one root (first entry with parentId === null). + * Orphaned entries (broken parent chain) are also returned as roots. + */ + getTree(): SessionTreeNode[] { + const entries = this.getEntries(); + const nodeMap = new Map(); + const roots: SessionTreeNode[] = []; + + // Create nodes with resolved labels + for (const entry of entries) { + const label = this.labelsById.get(entry.id); + const labelTimestamp = this.labelTimestampsById.get(entry.id); + nodeMap.set(entry.id, { entry, children: [], label, labelTimestamp }); + } + + // Build tree + for (const entry of entries) { + const node = nodeMap.get(entry.id)!; + if (entry.parentId === null || entry.parentId === entry.id) { + roots.push(node); + } else { + const parent = nodeMap.get(entry.parentId); + if (parent) { + parent.children.push(node); + } else { + // Orphan - treat as root + roots.push(node); + } + } + } + + // Sort children by timestamp (oldest first, newest at bottom) + // Use iterative approach to avoid stack overflow on deep trees + const stack: SessionTreeNode[] = [...roots]; + while (stack.length > 0) { + const node = stack.pop()!; + node.children.sort( + (a, b) => new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime(), + ); + stack.push(...node.children); + } + + return roots; + } + + // ========================================================================= + // Branching + // ========================================================================= + + /** + * Start a new branch from an earlier entry. + * Moves the leaf pointer to the specified entry. The next appendXXX() call + * will create a child of that entry, forming a new branch. Existing entries + * are not modified or deleted. + */ + branch(branchFromId: string): void { + if (!this.byId.has(branchFromId)) { + throw new Error(`Entry ${branchFromId} not found`); + } + this.leafId = branchFromId; + } + + /** + * Reset the leaf pointer to null (before any entries). + * The next appendXXX() call will create a new root entry (parentId = null). + * Use this when navigating to re-edit the first user message. + */ + resetLeaf(): void { + this.leafId = null; + } + + /** + * Start a new branch with a summary of the abandoned path. + * Same as branch(), but also appends a branch_summary entry that captures + * context from the abandoned conversation path. + */ + branchWithSummary( + branchFromId: string | null, + summary: string, + details?: unknown, + fromHook?: boolean, + ): string { + if (branchFromId !== null && !this.byId.has(branchFromId)) { + throw new Error(`Entry ${branchFromId} not found`); + } + this.leafId = branchFromId; + const entry: BranchSummaryEntry = { + type: "branch_summary", + id: generateId(this.byId), + parentId: branchFromId, + timestamp: new Date().toISOString(), + fromId: branchFromId ?? "root", + summary, + details, + fromHook, + }; + this.appendEntry(entry); + return entry.id; + } + + /** + * Create a new session file containing only the path from root to the specified leaf. + * Useful for extracting a single conversation path from a branched session. + * Returns the new session file path, or undefined if not persisting. + */ + createBranchedSession(leafId: string): string | undefined { + const previousSessionFile = this.sessionFile; + const path = this.getBranch(leafId); + if (path.length === 0) { + throw new Error(`Entry ${leafId} not found`); + } + + // Filter out LabelEntry from path - we'll recreate them from the resolved map + const pathWithoutLabels = path.filter((e) => e.type !== "label"); + + const newSessionId = createSessionId(); + const timestamp = new Date().toISOString(); + const fileTimestamp = timestamp.replace(/[:.]/g, "-"); + const newSessionFile = join(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`); + + const header: SessionHeader = { + type: "session", + version: CURRENT_SESSION_VERSION, + id: newSessionId, + timestamp, + cwd: this.cwd, + parentSession: this.shouldPersist ? previousSessionFile : undefined, + }; + + // Collect labels for entries in the path + const pathEntryIds = new Set(pathWithoutLabels.map((e) => e.id)); + const labelsToWrite: Array<{ targetId: string; label: string; timestamp: string }> = []; + for (const [targetId, label] of this.labelsById) { + if (pathEntryIds.has(targetId)) { + labelsToWrite.push({ targetId, label, timestamp: this.labelTimestampsById.get(targetId)! }); + } + } + + if (this.shouldPersist) { + // Build label entries + const lastEntryId = pathWithoutLabels[pathWithoutLabels.length - 1]?.id || null; + let parentId = lastEntryId; + const labelEntries: LabelEntry[] = []; + for (const { targetId, label, timestamp: labelTimestamp } of labelsToWrite) { + const labelEntry: LabelEntry = { + type: "label", + id: generateId(new Set(pathEntryIds)), + parentId, + timestamp: labelTimestamp, + targetId, + label, + }; + pathEntryIds.add(labelEntry.id); + labelEntries.push(labelEntry); + parentId = labelEntry.id; + } + + this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries]; + this.sessionId = newSessionId; + this.sessionFile = newSessionFile; + this.buildIndex(); + + // Only write the file now if it contains an assistant message. + // Otherwise defer to persist(), which creates the file on the + // first assistant response, matching the newSession() contract + // and avoiding the duplicate-header bug when persist()'s + // no-assistant guard later resets flushed to false. + const hasAssistant = this.fileEntries.some( + (e) => e.type === "message" && e.message.role === "assistant", + ); + if (hasAssistant) { + this.rewriteFile(); + this.flushed = true; + } else { + this.flushed = false; + } + + return newSessionFile; + } + + // In-memory mode: replace current session with the path + labels + const labelEntries: LabelEntry[] = []; + let parentId = pathWithoutLabels[pathWithoutLabels.length - 1]?.id || null; + for (const { targetId, label, timestamp: labelTimestamp } of labelsToWrite) { + const labelEntry: LabelEntry = { + type: "label", + id: generateId(new Set([...pathEntryIds, ...labelEntries.map((e) => e.id)])), + parentId, + timestamp: labelTimestamp, + targetId, + label, + }; + labelEntries.push(labelEntry); + parentId = labelEntry.id; + } + this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries]; + this.sessionId = newSessionId; + this.buildIndex(); + return undefined; + } + + /** + * Create a new session. + * @param cwd Working directory (stored in session header) + * @param sessionDir Optional session directory. If omitted, uses default (~/.openclaw/agent/sessions//). + */ + static create(cwd: string, sessionDir?: string): SessionManager { + const dir = sessionDir ?? getDefaultSessionDir(cwd); + return new SessionManager(cwd, dir, undefined, true); + } + + /** + * Open a specific session file. + * @param path Path to session file + * @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent. + * @param cwdOverride Optional cwd override instead of the session header cwd. + */ + static open(path: string, sessionDir?: string, cwdOverride?: string): SessionManager { + // Extract cwd from session header if possible, otherwise use process.cwd() + const entries = loadEntriesFromFile(path); + const header = entries.find((e) => e.type === "session"); + const cwd = cwdOverride ?? header?.cwd ?? process.cwd(); + // If no sessionDir provided, derive from file's parent directory + const dir = sessionDir ?? resolve(path, ".."); + return new SessionManager(cwd, dir, path, true); + } + + /** + * Continue the most recent session, or create new if none. + * @param cwd Working directory + * @param sessionDir Optional session directory. If omitted, uses default (~/.openclaw/agent/sessions//). + */ + static continueRecent(cwd: string, sessionDir?: string): SessionManager { + const dir = sessionDir ?? getDefaultSessionDir(cwd); + const mostRecent = findMostRecentSession(dir); + if (mostRecent) { + return new SessionManager(cwd, dir, mostRecent, true); + } + return new SessionManager(cwd, dir, undefined, true); + } + + /** Create an in-memory session (no file persistence) */ + static inMemory(cwd: string = process.cwd()): SessionManager { + return new SessionManager(cwd, "", undefined, false); + } + + /** + * Fork a session from another project directory into the current project. + * Creates a new session in the target cwd with the full history from the source session. + * @param sourcePath Path to the source session file + * @param targetCwd Target working directory (where the new session will be stored) + * @param sessionDir Optional session directory. If omitted, uses default for targetCwd. + */ + static forkFrom(sourcePath: string, targetCwd: string, sessionDir?: string): SessionManager { + const sourceEntries = loadEntriesFromFile(sourcePath); + if (sourceEntries.length === 0) { + throw new Error(`Cannot fork: source session file is empty or invalid: ${sourcePath}`); + } + + const sourceHeader = sourceEntries.find((e) => e.type === "session"); + if (!sourceHeader) { + throw new Error(`Cannot fork: source session has no header: ${sourcePath}`); + } + + const dir = sessionDir ?? getDefaultSessionDir(targetCwd); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + // Create new session file with new ID but forked content + const newSessionId = createSessionId(); + const timestamp = new Date().toISOString(); + const fileTimestamp = timestamp.replace(/[:.]/g, "-"); + const newSessionFile = join(dir, `${fileTimestamp}_${newSessionId}.jsonl`); + + // Write new header pointing to source as parent, with updated cwd + const newHeader: SessionHeader = { + type: "session", + version: CURRENT_SESSION_VERSION, + id: newSessionId, + timestamp, + cwd: targetCwd, + parentSession: sourcePath, + }; + appendFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`); + + // Copy all non-header entries from source + for (const entry of sourceEntries) { + if (entry.type !== "session") { + appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`); + } + } + + return new SessionManager(targetCwd, dir, newSessionFile, true); + } + + /** + * List all sessions for a directory. + * @param cwd Working directory (used to compute default session directory) + * @param sessionDir Optional session directory. If omitted, uses default (~/.openclaw/agent/sessions//). + * @param onProgress Optional callback for progress updates (loaded, total) + */ + static async list( + cwd: string, + sessionDir?: string, + onProgress?: SessionListProgress, + ): Promise { + const dir = sessionDir ?? getDefaultSessionDir(cwd); + const sessions = await listSessionsFromDir(dir, onProgress); + sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime()); + return sessions; + } + + /** + * List all sessions across all project directories. + * @param onProgress Optional callback for progress updates (loaded, total) + */ + static async listAll(onProgress?: SessionListProgress): Promise { + const sessionsDir = getSessionsDir(); + + try { + if (!existsSync(sessionsDir)) { + return []; + } + const entries = await readdir(sessionsDir, { withFileTypes: true }); + const dirs = entries.filter((e) => e.isDirectory()).map((e) => join(sessionsDir, e.name)); + + // Count total files first for accurate progress + let totalFiles = 0; + const dirFiles: string[][] = []; + for (const dir of dirs) { + try { + const files = (await readdir(dir)).filter((f) => f.endsWith(".jsonl")); + dirFiles.push(files.map((f) => join(dir, f))); + totalFiles += files.length; + } catch { + dirFiles.push([]); + } + } + + // Process all files with progress tracking + let loaded = 0; + const sessions: SessionInfo[] = []; + const allFiles = dirFiles.flat(); + + const results = await buildSessionInfosWithConcurrency(allFiles, () => { + loaded++; + onProgress?.(loaded, totalFiles); + }); + + for (const info of results) { + if (info) { + sessions.push(info); + } + } + + sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime()); + return sessions; + } catch { + return []; + } + } +} diff --git a/src/agents/sessions/settings-manager.ts b/src/agents/sessions/settings-manager.ts new file mode 100644 index 00000000000..e2cc544367f --- /dev/null +++ b/src/agents/sessions/settings-manager.ts @@ -0,0 +1,1111 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; +import type { Transport } from "openclaw/plugin-sdk/llm"; +import lockfile from "proper-lockfile"; +import { CONFIG_DIR_NAME, getAgentDir } from "../config.js"; +import { DEFAULT_HTTP_IDLE_TIMEOUT_MS, parseHttpIdleTimeoutMs } from "./http-dispatcher.js"; + +export interface CompactionSettings { + enabled?: boolean; // default: true + reserveTokens?: number; // default: 16384 + keepRecentTokens?: number; // default: 20000 +} + +export interface BranchSummarySettings { + reserveTokens?: number; // default: 16384 (tokens reserved for prompt + LLM response) + skipPrompt?: boolean; // default: false - when true, skips "Summarize branch?" prompt and defaults to no summary +} + +export interface ProviderRetrySettings { + timeoutMs?: number; // SDK/provider request timeout in milliseconds + maxRetries?: number; // SDK/provider retry attempts + maxRetryDelayMs?: number; // default: 60000 (max server-requested delay before failing) +} + +export interface RetrySettings { + enabled?: boolean; // default: true + maxRetries?: number; // default: 3 + baseDelayMs?: number; // default: 2000 (exponential backoff: 2s, 4s, 8s) + provider?: ProviderRetrySettings; +} + +export interface TerminalSettings { + showImages?: boolean; // default: true (only relevant if terminal supports images) + imageWidthCells?: number; // default: 60 (preferred inline image width in terminal cells) + clearOnShrink?: boolean; // default: false (clear empty rows when content shrinks) + showTerminalProgress?: boolean; // default: false (OSC 9;4 terminal progress indicators) +} + +export interface ImageSettings { + autoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility) + blockImages?: boolean; // default: false - when true, prevents all images from being sent to LLM providers +} + +export interface ThinkingBudgetsSettings { + minimal?: number; + low?: number; + medium?: number; + high?: number; +} + +export interface MarkdownSettings { + codeBlockIndent?: string; // default: " " +} + +export interface WarningSettings { + anthropicExtraUsage?: boolean; // default: true +} + +export type TransportSetting = Transport; + +/** + * Package source for npm/git packages. + * - String form: load all resources from the package + * - Object form: filter which resources to load + */ +export type PackageSource = + | string + | { + source: string; + extensions?: string[]; + skills?: string[]; + prompts?: string[]; + themes?: string[]; + }; + +export interface Settings { + lastChangelogVersion?: string; + defaultProvider?: string; + defaultModel?: string; + defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; + transport?: TransportSetting; // default: "auto" + steeringMode?: "all" | "one-at-a-time"; + followUpMode?: "all" | "one-at-a-time"; + theme?: string; + compaction?: CompactionSettings; + branchSummary?: BranchSummarySettings; + retry?: RetrySettings; + hideThinkingBlock?: boolean; + shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows) + quietStartup?: boolean; + shellCommandPrefix?: string; // Prefix prepended to every bash command (e.g., "shopt -s expand_aliases" for alias support) + npmCommand?: string[]; // Command used for npm package lookup/install operations, argv-style (e.g., ["mise", "exec", "node@20", "--", "npm"]) + collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full) + enableInstallTelemetry?: boolean; // default: true - anonymous version/update ping after changelog-detected updates + packages?: PackageSource[]; // Array of npm/git package sources (string or object with filtering) + extensions?: string[]; // Array of local extension file paths or directories + skills?: string[]; // Array of local skill file paths or directories + prompts?: string[]; // Array of local prompt template paths or directories + themes?: string[]; // Array of local theme file paths or directories + enableSkillCommands?: boolean; // default: true - register skills as /skill:name commands + terminal?: TerminalSettings; + images?: ImageSettings; + enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag) + doubleEscapeAction?: "fork" | "tree" | "none"; // Action for double-escape with empty editor (default: "tree") + treeFilterMode?: "default" | "no-tools" | "user-only" | "labeled-only" | "all"; // Default filter when opening /tree + thinkingBudgets?: ThinkingBudgetsSettings; // Custom token budgets for thinking levels + editorPaddingX?: number; // Horizontal padding for input editor (default: 0) + autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5) + showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME + markdown?: MarkdownSettings; + warnings?: WarningSettings; + sessionDir?: string; // Custom session storage directory (same format as --session-dir CLI flag) + httpIdleTimeoutMs?: number; // HTTP header/body idle timeout in milliseconds; 0 disables it +} + +/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */ +function deepMergeSettings(base: Settings, overrides: Settings): Settings { + const result: Settings = { ...base }; + + for (const key of Object.keys(overrides) as (keyof Settings)[]) { + const overrideValue = overrides[key]; + const baseValue = base[key]; + + if (overrideValue === undefined) { + continue; + } + + // For nested objects, merge recursively + if ( + typeof overrideValue === "object" && + overrideValue !== null && + !Array.isArray(overrideValue) && + typeof baseValue === "object" && + baseValue !== null && + !Array.isArray(baseValue) + ) { + (result as Record)[key] = { ...baseValue, ...overrideValue }; + } else { + // For primitives and arrays, override value wins + (result as Record)[key] = overrideValue; + } + } + + return result; +} + +export type SettingsScope = "global" | "project"; + +export interface SettingsStorage { + withLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void; +} + +export interface SettingsError { + scope: SettingsScope; + error: Error; +} + +export class FileSettingsStorage implements SettingsStorage { + private globalSettingsPath: string; + private projectSettingsPath: string; + + constructor(cwd: string, agentDir: string) { + this.globalSettingsPath = join(agentDir, "settings.json"); + this.projectSettingsPath = join(cwd, CONFIG_DIR_NAME, "settings.json"); + } + + private acquireLockSyncWithRetry(path: string): () => void { + const maxAttempts = 10; + const delayMs = 20; + let lastError: unknown; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return lockfile.lockSync(path, { realpath: false }); + } catch (error) { + const code = + typeof error === "object" && error !== null && "code" in error + ? String((error as { code?: unknown }).code) + : undefined; + if (code !== "ELOCKED" || attempt === maxAttempts) { + throw error; + } + lastError = error; + const start = Date.now(); + while (Date.now() - start < delayMs) { + // Sleep synchronously to avoid changing callers to async. + } + } + } + + throw (lastError as Error) ?? new Error("Failed to acquire settings lock"); + } + + withLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void { + const path = scope === "global" ? this.globalSettingsPath : this.projectSettingsPath; + const dir = dirname(path); + + let release: (() => void) | undefined; + try { + // Only create directory and lock if file exists or we need to write + const fileExists = existsSync(path); + if (fileExists) { + release = this.acquireLockSyncWithRetry(path); + } + const current = fileExists ? readFileSync(path, "utf-8") : undefined; + const next = fn(current); + if (next !== undefined) { + // Only create directory when we actually need to write + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + if (!release) { + release = this.acquireLockSyncWithRetry(path); + } + writeFileSync(path, next, "utf-8"); + } + } finally { + if (release) { + release(); + } + } + } +} + +export class InMemorySettingsStorage implements SettingsStorage { + private global: string | undefined; + private project: string | undefined; + + withLock(scope: SettingsScope, fn: (current: string | undefined) => string | undefined): void { + const current = scope === "global" ? this.global : this.project; + const next = fn(current); + if (next !== undefined) { + if (scope === "global") { + this.global = next; + } else { + this.project = next; + } + } + } +} + +export class SettingsManager { + private storage: SettingsStorage; + private globalSettings: Settings; + private projectSettings: Settings; + private settings: Settings; + private modifiedFields = new Set(); // Track global fields modified during session + private modifiedNestedFields = new Map>(); // Track global nested field modifications + private modifiedProjectFields = new Set(); // Track project fields modified during session + private modifiedProjectNestedFields = new Map>(); // Track project nested field modifications + private globalSettingsLoadError: Error | null = null; // Track if global settings file had parse errors + private projectSettingsLoadError: Error | null = null; // Track if project settings file had parse errors + private writeQueue: Promise = Promise.resolve(); + private errors: SettingsError[]; + + private constructor( + storage: SettingsStorage, + initialGlobal: Settings, + initialProject: Settings, + globalLoadError: Error | null = null, + projectLoadError: Error | null = null, + initialErrors: SettingsError[] = [], + ) { + this.storage = storage; + this.globalSettings = initialGlobal; + this.projectSettings = initialProject; + this.globalSettingsLoadError = globalLoadError; + this.projectSettingsLoadError = projectLoadError; + this.errors = [...initialErrors]; + this.settings = deepMergeSettings(this.globalSettings, this.projectSettings); + } + + /** Create a SettingsManager that loads from files */ + static create(cwd: string, agentDir: string = getAgentDir()): SettingsManager { + const storage = new FileSettingsStorage(cwd, agentDir); + return SettingsManager.fromStorage(storage); + } + + /** Create a SettingsManager from an arbitrary storage backend */ + static fromStorage(storage: SettingsStorage): SettingsManager { + const globalLoad = SettingsManager.tryLoadFromStorage(storage, "global"); + const projectLoad = SettingsManager.tryLoadFromStorage(storage, "project"); + const initialErrors: SettingsError[] = []; + if (globalLoad.error) { + initialErrors.push({ scope: "global", error: globalLoad.error }); + } + if (projectLoad.error) { + initialErrors.push({ scope: "project", error: projectLoad.error }); + } + + return new SettingsManager( + storage, + globalLoad.settings, + projectLoad.settings, + globalLoad.error, + projectLoad.error, + initialErrors, + ); + } + + /** Create an in-memory SettingsManager (no file I/O) */ + static inMemory(settings: Partial = {}): SettingsManager { + const storage = new InMemorySettingsStorage(); + const initialSettings = SettingsManager.migrateSettings( + structuredClone(settings) as Record, + ); + storage.withLock("global", () => JSON.stringify(initialSettings, null, 2)); + return SettingsManager.fromStorage(storage); + } + + private static loadFromStorage(storage: SettingsStorage, scope: SettingsScope): Settings { + let content: string | undefined; + storage.withLock(scope, (current) => { + content = current; + return undefined; + }); + + if (!content) { + return {}; + } + const settings = JSON.parse(content); + return SettingsManager.migrateSettings(settings); + } + + private static tryLoadFromStorage( + storage: SettingsStorage, + scope: SettingsScope, + ): { settings: Settings; error: Error | null } { + try { + return { settings: SettingsManager.loadFromStorage(storage, scope), error: null }; + } catch (error) { + return { settings: {}, error: error as Error }; + } + } + + /** Migrate old settings format to new format */ + private static migrateSettings(settings: Record): Settings { + // Migrate queueMode -> steeringMode + if ("queueMode" in settings && !("steeringMode" in settings)) { + settings.steeringMode = settings.queueMode; + delete settings.queueMode; + } + + // Migrate legacy websockets boolean -> transport enum + if (!("transport" in settings) && typeof settings.websockets === "boolean") { + settings.transport = settings.websockets ? "websocket" : "sse"; + delete settings.websockets; + } + + // Migrate old skills object format to new array format + if ( + "skills" in settings && + typeof settings.skills === "object" && + settings.skills !== null && + !Array.isArray(settings.skills) + ) { + const skillsSettings = settings.skills as { + enableSkillCommands?: boolean; + customDirectories?: unknown; + }; + if ( + skillsSettings.enableSkillCommands !== undefined && + settings.enableSkillCommands === undefined + ) { + settings.enableSkillCommands = skillsSettings.enableSkillCommands; + } + if ( + Array.isArray(skillsSettings.customDirectories) && + skillsSettings.customDirectories.length > 0 + ) { + settings.skills = skillsSettings.customDirectories; + } else { + delete settings.skills; + } + } + + // Migrate retry.maxDelayMs -> retry.provider.maxRetryDelayMs + if ( + "retry" in settings && + typeof settings.retry === "object" && + settings.retry !== null && + !Array.isArray(settings.retry) + ) { + const retrySettings = settings.retry as Record; + const providerSettings = + typeof retrySettings.provider === "object" && retrySettings.provider !== null + ? (retrySettings.provider as Record) + : undefined; + if ( + typeof retrySettings.maxDelayMs === "number" && + (providerSettings?.maxRetryDelayMs === undefined || + providerSettings?.maxRetryDelayMs === null) + ) { + retrySettings.provider = { + ...providerSettings, + maxRetryDelayMs: retrySettings.maxDelayMs, + }; + } + delete retrySettings.maxDelayMs; + } + + return settings as Settings; + } + + getGlobalSettings(): Settings { + return structuredClone(this.globalSettings); + } + + getProjectSettings(): Settings { + return structuredClone(this.projectSettings); + } + + async reload(): Promise { + await this.writeQueue; + const globalLoad = SettingsManager.tryLoadFromStorage(this.storage, "global"); + if (!globalLoad.error) { + this.globalSettings = globalLoad.settings; + this.globalSettingsLoadError = null; + } else { + this.globalSettingsLoadError = globalLoad.error; + this.recordError("global", globalLoad.error); + } + + this.modifiedFields.clear(); + this.modifiedNestedFields.clear(); + this.modifiedProjectFields.clear(); + this.modifiedProjectNestedFields.clear(); + + const projectLoad = SettingsManager.tryLoadFromStorage(this.storage, "project"); + if (!projectLoad.error) { + this.projectSettings = projectLoad.settings; + this.projectSettingsLoadError = null; + } else { + this.projectSettingsLoadError = projectLoad.error; + this.recordError("project", projectLoad.error); + } + + this.settings = deepMergeSettings(this.globalSettings, this.projectSettings); + } + + /** Apply additional overrides on top of current settings */ + applyOverrides(overrides: Partial): void { + this.settings = deepMergeSettings(this.settings, overrides); + } + + /** Mark a global field as modified during this session */ + private markModified(field: keyof Settings, nestedKey?: string): void { + this.modifiedFields.add(field); + if (nestedKey) { + if (!this.modifiedNestedFields.has(field)) { + this.modifiedNestedFields.set(field, new Set()); + } + this.modifiedNestedFields.get(field)!.add(nestedKey); + } + } + + /** Mark a project field as modified during this session */ + private markProjectModified(field: keyof Settings, nestedKey?: string): void { + this.modifiedProjectFields.add(field); + if (nestedKey) { + if (!this.modifiedProjectNestedFields.has(field)) { + this.modifiedProjectNestedFields.set(field, new Set()); + } + this.modifiedProjectNestedFields.get(field)!.add(nestedKey); + } + } + + private recordError(scope: SettingsScope, error: unknown): void { + const normalizedError = error instanceof Error ? error : new Error(String(error)); + this.errors.push({ scope, error: normalizedError }); + } + + private clearModifiedScope(scope: SettingsScope): void { + if (scope === "global") { + this.modifiedFields.clear(); + this.modifiedNestedFields.clear(); + return; + } + + this.modifiedProjectFields.clear(); + this.modifiedProjectNestedFields.clear(); + } + + private enqueueWrite(scope: SettingsScope, task: () => void): void { + this.writeQueue = this.writeQueue + .then(() => { + task(); + this.clearModifiedScope(scope); + }) + .catch((error) => { + this.recordError(scope, error); + }); + } + + private cloneModifiedNestedFields( + source: Map>, + ): Map> { + const snapshot = new Map>(); + for (const [key, value] of source.entries()) { + snapshot.set(key, new Set(value)); + } + return snapshot; + } + + private persistScopedSettings( + scope: SettingsScope, + snapshotSettings: Settings, + modifiedFields: Set, + modifiedNestedFields: Map>, + ): void { + this.storage.withLock(scope, (current) => { + const currentFileSettings = current + ? SettingsManager.migrateSettings(JSON.parse(current) as Record) + : {}; + const mergedSettings: Settings = { ...currentFileSettings }; + for (const field of modifiedFields) { + const value = snapshotSettings[field]; + if (modifiedNestedFields.has(field) && typeof value === "object" && value !== null) { + const nestedModified = modifiedNestedFields.get(field)!; + const baseNested = (currentFileSettings[field] as Record) ?? {}; + const inMemoryNested = value as Record; + const mergedNested = { ...baseNested }; + for (const nestedKey of nestedModified) { + mergedNested[nestedKey] = inMemoryNested[nestedKey]; + } + (mergedSettings as Record)[field] = mergedNested; + } else { + (mergedSettings as Record)[field] = value; + } + } + + return JSON.stringify(mergedSettings, null, 2); + }); + } + + private save(): void { + this.settings = deepMergeSettings(this.globalSettings, this.projectSettings); + + if (this.globalSettingsLoadError) { + return; + } + + const snapshotGlobalSettings = structuredClone(this.globalSettings); + const modifiedFields = new Set(this.modifiedFields); + const modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedNestedFields); + + this.enqueueWrite("global", () => { + this.persistScopedSettings( + "global", + snapshotGlobalSettings, + modifiedFields, + modifiedNestedFields, + ); + }); + } + + private saveProjectSettings(settings: Settings): void { + this.projectSettings = structuredClone(settings); + this.settings = deepMergeSettings(this.globalSettings, this.projectSettings); + + if (this.projectSettingsLoadError) { + return; + } + + const snapshotProjectSettings = structuredClone(this.projectSettings); + const modifiedFields = new Set(this.modifiedProjectFields); + const modifiedNestedFields = this.cloneModifiedNestedFields(this.modifiedProjectNestedFields); + this.enqueueWrite("project", () => { + this.persistScopedSettings( + "project", + snapshotProjectSettings, + modifiedFields, + modifiedNestedFields, + ); + }); + } + + async flush(): Promise { + await this.writeQueue; + } + + drainErrors(): SettingsError[] { + const drained = [...this.errors]; + this.errors = []; + return drained; + } + + getLastChangelogVersion(): string | undefined { + return this.settings.lastChangelogVersion; + } + + setLastChangelogVersion(version: string): void { + this.globalSettings.lastChangelogVersion = version; + this.markModified("lastChangelogVersion"); + this.save(); + } + + getSessionDir(): string | undefined { + const sessionDir = this.settings.sessionDir; + if (!sessionDir) { + return sessionDir; + } + if (sessionDir === "~") { + return homedir(); + } + if (sessionDir.startsWith("~/")) { + return join(homedir(), sessionDir.slice(2)); + } + return sessionDir; + } + + getDefaultProvider(): string | undefined { + return this.settings.defaultProvider; + } + + getDefaultModel(): string | undefined { + return this.settings.defaultModel; + } + + setDefaultProvider(provider: string): void { + this.globalSettings.defaultProvider = provider; + this.markModified("defaultProvider"); + this.save(); + } + + setDefaultModel(modelId: string): void { + this.globalSettings.defaultModel = modelId; + this.markModified("defaultModel"); + this.save(); + } + + setDefaultModelAndProvider(provider: string, modelId: string): void { + this.globalSettings.defaultProvider = provider; + this.globalSettings.defaultModel = modelId; + this.markModified("defaultProvider"); + this.markModified("defaultModel"); + this.save(); + } + + getSteeringMode(): "all" | "one-at-a-time" { + return this.settings.steeringMode || "one-at-a-time"; + } + + setSteeringMode(mode: "all" | "one-at-a-time"): void { + this.globalSettings.steeringMode = mode; + this.markModified("steeringMode"); + this.save(); + } + + getFollowUpMode(): "all" | "one-at-a-time" { + return this.settings.followUpMode || "one-at-a-time"; + } + + setFollowUpMode(mode: "all" | "one-at-a-time"): void { + this.globalSettings.followUpMode = mode; + this.markModified("followUpMode"); + this.save(); + } + + getTheme(): string | undefined { + return this.settings.theme; + } + + setTheme(theme: string): void { + this.globalSettings.theme = theme; + this.markModified("theme"); + this.save(); + } + + getDefaultThinkingLevel(): "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | undefined { + return this.settings.defaultThinkingLevel; + } + + setDefaultThinkingLevel(level: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"): void { + this.globalSettings.defaultThinkingLevel = level; + this.markModified("defaultThinkingLevel"); + this.save(); + } + + getTransport(): TransportSetting { + return this.settings.transport ?? "auto"; + } + + setTransport(transport: TransportSetting): void { + this.globalSettings.transport = transport; + this.markModified("transport"); + this.save(); + } + + getCompactionEnabled(): boolean { + return this.settings.compaction?.enabled ?? true; + } + + setCompactionEnabled(enabled: boolean): void { + if (!this.globalSettings.compaction) { + this.globalSettings.compaction = {}; + } + this.globalSettings.compaction.enabled = enabled; + this.markModified("compaction", "enabled"); + this.save(); + } + + getCompactionReserveTokens(): number { + return this.settings.compaction?.reserveTokens ?? 16384; + } + + getCompactionKeepRecentTokens(): number { + return this.settings.compaction?.keepRecentTokens ?? 20000; + } + + getCompactionSettings(): { enabled: boolean; reserveTokens: number; keepRecentTokens: number } { + return { + enabled: this.getCompactionEnabled(), + reserveTokens: this.getCompactionReserveTokens(), + keepRecentTokens: this.getCompactionKeepRecentTokens(), + }; + } + + getBranchSummarySettings(): { reserveTokens: number; skipPrompt: boolean } { + return { + reserveTokens: this.settings.branchSummary?.reserveTokens ?? 16384, + skipPrompt: this.settings.branchSummary?.skipPrompt ?? false, + }; + } + + getBranchSummarySkipPrompt(): boolean { + return this.settings.branchSummary?.skipPrompt ?? false; + } + + getRetryEnabled(): boolean { + return this.settings.retry?.enabled ?? true; + } + + setRetryEnabled(enabled: boolean): void { + if (!this.globalSettings.retry) { + this.globalSettings.retry = {}; + } + this.globalSettings.retry.enabled = enabled; + this.markModified("retry", "enabled"); + this.save(); + } + + getRetrySettings(): { enabled: boolean; maxRetries: number; baseDelayMs: number } { + return { + enabled: this.getRetryEnabled(), + maxRetries: this.settings.retry?.maxRetries ?? 3, + baseDelayMs: this.settings.retry?.baseDelayMs ?? 2000, + }; + } + + getHttpIdleTimeoutMs(): number { + const value = this.settings.httpIdleTimeoutMs; + const timeoutMs = parseHttpIdleTimeoutMs(value); + if (timeoutMs !== undefined) { + return timeoutMs; + } + if (value !== undefined) { + throw new Error(`Invalid httpIdleTimeoutMs setting: ${String(value)}`); + } + return DEFAULT_HTTP_IDLE_TIMEOUT_MS; + } + + setHttpIdleTimeoutMs(timeoutMs: number): void { + if (!Number.isFinite(timeoutMs) || timeoutMs < 0) { + throw new Error(`Invalid httpIdleTimeoutMs setting: ${String(timeoutMs)}`); + } + this.globalSettings.httpIdleTimeoutMs = Math.floor(timeoutMs); + this.markModified("httpIdleTimeoutMs"); + this.save(); + } + + getProviderRetrySettings(): { timeoutMs?: number; maxRetries?: number; maxRetryDelayMs: number } { + return { + timeoutMs: this.settings.retry?.provider?.timeoutMs, + maxRetries: this.settings.retry?.provider?.maxRetries, + maxRetryDelayMs: this.settings.retry?.provider?.maxRetryDelayMs ?? 60000, + }; + } + + getHideThinkingBlock(): boolean { + return this.settings.hideThinkingBlock ?? false; + } + + setHideThinkingBlock(hide: boolean): void { + this.globalSettings.hideThinkingBlock = hide; + this.markModified("hideThinkingBlock"); + this.save(); + } + + getShellPath(): string | undefined { + return this.settings.shellPath; + } + + setShellPath(path: string | undefined): void { + this.globalSettings.shellPath = path; + this.markModified("shellPath"); + this.save(); + } + + getQuietStartup(): boolean { + return this.settings.quietStartup ?? false; + } + + setQuietStartup(quiet: boolean): void { + this.globalSettings.quietStartup = quiet; + this.markModified("quietStartup"); + this.save(); + } + + getShellCommandPrefix(): string | undefined { + return this.settings.shellCommandPrefix; + } + + setShellCommandPrefix(prefix: string | undefined): void { + this.globalSettings.shellCommandPrefix = prefix; + this.markModified("shellCommandPrefix"); + this.save(); + } + + getNpmCommand(): string[] | undefined { + return this.settings.npmCommand ? [...this.settings.npmCommand] : undefined; + } + + setNpmCommand(command: string[] | undefined): void { + this.globalSettings.npmCommand = command ? [...command] : undefined; + this.markModified("npmCommand"); + this.save(); + } + + getCollapseChangelog(): boolean { + return this.settings.collapseChangelog ?? false; + } + + setCollapseChangelog(collapse: boolean): void { + this.globalSettings.collapseChangelog = collapse; + this.markModified("collapseChangelog"); + this.save(); + } + + getEnableInstallTelemetry(): boolean { + return this.settings.enableInstallTelemetry ?? true; + } + + setEnableInstallTelemetry(enabled: boolean): void { + this.globalSettings.enableInstallTelemetry = enabled; + this.markModified("enableInstallTelemetry"); + this.save(); + } + + getPackages(): PackageSource[] { + return [...(this.settings.packages ?? [])]; + } + + setPackages(packages: PackageSource[]): void { + this.globalSettings.packages = packages; + this.markModified("packages"); + this.save(); + } + + setProjectPackages(packages: PackageSource[]): void { + const projectSettings = structuredClone(this.projectSettings); + projectSettings.packages = packages; + this.markProjectModified("packages"); + this.saveProjectSettings(projectSettings); + } + + getExtensionPaths(): string[] { + return [...(this.settings.extensions ?? [])]; + } + + setExtensionPaths(paths: string[]): void { + this.globalSettings.extensions = paths; + this.markModified("extensions"); + this.save(); + } + + setProjectExtensionPaths(paths: string[]): void { + const projectSettings = structuredClone(this.projectSettings); + projectSettings.extensions = paths; + this.markProjectModified("extensions"); + this.saveProjectSettings(projectSettings); + } + + getSkillPaths(): string[] { + return [...(this.settings.skills ?? [])]; + } + + setSkillPaths(paths: string[]): void { + this.globalSettings.skills = paths; + this.markModified("skills"); + this.save(); + } + + setProjectSkillPaths(paths: string[]): void { + const projectSettings = structuredClone(this.projectSettings); + projectSettings.skills = paths; + this.markProjectModified("skills"); + this.saveProjectSettings(projectSettings); + } + + getPromptTemplatePaths(): string[] { + return [...(this.settings.prompts ?? [])]; + } + + setPromptTemplatePaths(paths: string[]): void { + this.globalSettings.prompts = paths; + this.markModified("prompts"); + this.save(); + } + + setProjectPromptTemplatePaths(paths: string[]): void { + const projectSettings = structuredClone(this.projectSettings); + projectSettings.prompts = paths; + this.markProjectModified("prompts"); + this.saveProjectSettings(projectSettings); + } + + getThemePaths(): string[] { + return [...(this.settings.themes ?? [])]; + } + + setThemePaths(paths: string[]): void { + this.globalSettings.themes = paths; + this.markModified("themes"); + this.save(); + } + + setProjectThemePaths(paths: string[]): void { + const projectSettings = structuredClone(this.projectSettings); + projectSettings.themes = paths; + this.markProjectModified("themes"); + this.saveProjectSettings(projectSettings); + } + + getEnableSkillCommands(): boolean { + return this.settings.enableSkillCommands ?? true; + } + + setEnableSkillCommands(enabled: boolean): void { + this.globalSettings.enableSkillCommands = enabled; + this.markModified("enableSkillCommands"); + this.save(); + } + + getThinkingBudgets(): ThinkingBudgetsSettings | undefined { + return this.settings.thinkingBudgets; + } + + getShowImages(): boolean { + return this.settings.terminal?.showImages ?? true; + } + + setShowImages(show: boolean): void { + if (!this.globalSettings.terminal) { + this.globalSettings.terminal = {}; + } + this.globalSettings.terminal.showImages = show; + this.markModified("terminal", "showImages"); + this.save(); + } + + getImageWidthCells(): number { + const width = this.settings.terminal?.imageWidthCells; + if (typeof width !== "number" || !Number.isFinite(width)) { + return 60; + } + return Math.max(1, Math.floor(width)); + } + + setImageWidthCells(width: number): void { + if (!this.globalSettings.terminal) { + this.globalSettings.terminal = {}; + } + this.globalSettings.terminal.imageWidthCells = Math.max(1, Math.floor(width)); + this.markModified("terminal", "imageWidthCells"); + this.save(); + } + + getClearOnShrink(): boolean { + // Settings takes precedence, then env var, then default false + if (this.settings.terminal?.clearOnShrink !== undefined) { + return this.settings.terminal.clearOnShrink; + } + return process.env.OPENCLAW_CLEAR_ON_SHRINK === "1"; + } + + setClearOnShrink(enabled: boolean): void { + if (!this.globalSettings.terminal) { + this.globalSettings.terminal = {}; + } + this.globalSettings.terminal.clearOnShrink = enabled; + this.markModified("terminal", "clearOnShrink"); + this.save(); + } + + getShowTerminalProgress(): boolean { + return this.settings.terminal?.showTerminalProgress ?? false; + } + + setShowTerminalProgress(enabled: boolean): void { + if (!this.globalSettings.terminal) { + this.globalSettings.terminal = {}; + } + this.globalSettings.terminal.showTerminalProgress = enabled; + this.markModified("terminal", "showTerminalProgress"); + this.save(); + } + + getImageAutoResize(): boolean { + return this.settings.images?.autoResize ?? true; + } + + setImageAutoResize(enabled: boolean): void { + if (!this.globalSettings.images) { + this.globalSettings.images = {}; + } + this.globalSettings.images.autoResize = enabled; + this.markModified("images", "autoResize"); + this.save(); + } + + getBlockImages(): boolean { + return this.settings.images?.blockImages ?? false; + } + + setBlockImages(blocked: boolean): void { + if (!this.globalSettings.images) { + this.globalSettings.images = {}; + } + this.globalSettings.images.blockImages = blocked; + this.markModified("images", "blockImages"); + this.save(); + } + + getEnabledModels(): string[] | undefined { + return this.settings.enabledModels; + } + + setEnabledModels(patterns: string[] | undefined): void { + this.globalSettings.enabledModels = patterns; + this.markModified("enabledModels"); + this.save(); + } + + getDoubleEscapeAction(): "fork" | "tree" | "none" { + return this.settings.doubleEscapeAction ?? "tree"; + } + + setDoubleEscapeAction(action: "fork" | "tree" | "none"): void { + this.globalSettings.doubleEscapeAction = action; + this.markModified("doubleEscapeAction"); + this.save(); + } + + getTreeFilterMode(): "default" | "no-tools" | "user-only" | "labeled-only" | "all" { + const mode = this.settings.treeFilterMode; + const valid = ["default", "no-tools", "user-only", "labeled-only", "all"]; + return mode && valid.includes(mode) ? mode : "default"; + } + + setTreeFilterMode(mode: "default" | "no-tools" | "user-only" | "labeled-only" | "all"): void { + this.globalSettings.treeFilterMode = mode; + this.markModified("treeFilterMode"); + this.save(); + } + + getShowHardwareCursor(): boolean { + return this.settings.showHardwareCursor ?? process.env.OPENCLAW_HARDWARE_CURSOR === "1"; + } + + setShowHardwareCursor(enabled: boolean): void { + this.globalSettings.showHardwareCursor = enabled; + this.markModified("showHardwareCursor"); + this.save(); + } + + getEditorPaddingX(): number { + return this.settings.editorPaddingX ?? 0; + } + + setEditorPaddingX(padding: number): void { + this.globalSettings.editorPaddingX = Math.max(0, Math.min(3, Math.floor(padding))); + this.markModified("editorPaddingX"); + this.save(); + } + + getAutocompleteMaxVisible(): number { + return this.settings.autocompleteMaxVisible ?? 5; + } + + setAutocompleteMaxVisible(maxVisible: number): void { + this.globalSettings.autocompleteMaxVisible = Math.max(3, Math.min(20, Math.floor(maxVisible))); + this.markModified("autocompleteMaxVisible"); + this.save(); + } + + getCodeBlockIndent(): string { + return this.settings.markdown?.codeBlockIndent ?? " "; + } + + getWarnings(): WarningSettings { + return { ...this.settings.warnings }; + } + + setWarnings(warnings: WarningSettings): void { + this.globalSettings.warnings = { ...warnings }; + this.markModified("warnings"); + this.save(); + } +} diff --git a/src/agents/sessions/skills.ts b/src/agents/sessions/skills.ts new file mode 100644 index 00000000000..b36781482f9 --- /dev/null +++ b/src/agents/sessions/skills.ts @@ -0,0 +1,526 @@ +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path"; +import ignore from "ignore"; +import { CONFIG_DIR_NAME, getAgentDir } from "../config.js"; +import { parseFrontmatter } from "../utils/frontmatter.js"; +import { canonicalizePath } from "../utils/paths.js"; +import type { ResourceDiagnostic } from "./diagnostics.js"; +import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js"; + +/** Max name length per spec */ +const MAX_NAME_LENGTH = 64; + +/** Max description length per spec */ +const MAX_DESCRIPTION_LENGTH = 1024; + +const IGNORE_FILE_NAMES = [".gitignore", ".ignore", ".fdignore"]; + +type IgnoreMatcher = ReturnType; + +function toPosixPath(p: string): string { + return p.split(sep).join("/"); +} + +function prefixIgnorePattern(line: string, prefix: string): string | null { + const trimmed = line.trim(); + if (!trimmed) { + return null; + } + if (trimmed.startsWith("#") && !trimmed.startsWith("\\#")) { + return null; + } + + let pattern = line; + let negated = false; + + if (pattern.startsWith("!")) { + negated = true; + pattern = pattern.slice(1); + } else if (pattern.startsWith("\\!")) { + pattern = pattern.slice(1); + } + + if (pattern.startsWith("/")) { + pattern = pattern.slice(1); + } + + const prefixed = prefix ? `${prefix}${pattern}` : pattern; + return negated ? `!${prefixed}` : prefixed; +} + +function addIgnoreRules(ig: IgnoreMatcher, dir: string, rootDir: string): void { + const relativeDir = relative(rootDir, dir); + const prefix = relativeDir ? `${toPosixPath(relativeDir)}/` : ""; + + for (const filename of IGNORE_FILE_NAMES) { + const ignorePath = join(dir, filename); + if (!existsSync(ignorePath)) { + continue; + } + try { + const content = readFileSync(ignorePath, "utf-8"); + const patterns = content + .split(/\r?\n/) + .map((line) => prefixIgnorePattern(line, prefix)) + .filter((line): line is string => Boolean(line)); + if (patterns.length > 0) { + ig.add(patterns); + } + } catch {} + } +} + +export interface SkillFrontmatter { + name?: string; + description?: string; + "disable-model-invocation"?: boolean; + [key: string]: unknown; +} + +export interface Skill { + name: string; + description: string; + filePath: string; + baseDir: string; + source: string; + sourceInfo: SourceInfo; + disableModelInvocation: boolean; +} + +export interface LoadSkillsResult { + skills: Skill[]; + diagnostics: ResourceDiagnostic[]; +} + +/** + * Validate skill name per Agent Skills spec. + * Returns array of validation error messages (empty if valid). + */ +function validateName(name: string): string[] { + const errors: string[] = []; + + if (name.length > MAX_NAME_LENGTH) { + errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`); + } + + if (!/^[a-z0-9-]+$/.test(name)) { + errors.push(`name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)`); + } + + if (name.startsWith("-") || name.endsWith("-")) { + errors.push(`name must not start or end with a hyphen`); + } + + if (name.includes("--")) { + errors.push(`name must not contain consecutive hyphens`); + } + + return errors; +} + +/** + * Validate description per Agent Skills spec. + */ +function validateDescription(description: string | undefined): string[] { + const errors: string[] = []; + + if (!description || description.trim() === "") { + errors.push("description is required"); + } else if (description.length > MAX_DESCRIPTION_LENGTH) { + errors.push(`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`); + } + + return errors; +} + +export interface LoadSkillsFromDirOptions { + /** Directory to scan for skills */ + dir: string; + /** Source identifier for these skills */ + source: string; +} + +function createSkillSourceInfo(filePath: string, baseDir: string, source: string): SourceInfo { + switch (source) { + case "user": + return createSyntheticSourceInfo(filePath, { + source: "local", + scope: "user", + baseDir, + }); + case "project": + return createSyntheticSourceInfo(filePath, { + source: "local", + scope: "project", + baseDir, + }); + case "path": + return createSyntheticSourceInfo(filePath, { + source: "local", + baseDir, + }); + default: + return createSyntheticSourceInfo(filePath, { source, baseDir }); + } +} + +/** + * Load skills from a directory. + * + * Discovery rules: + * - if a directory contains SKILL.md, treat it as a skill root and do not recurse further + * - otherwise, load direct .md children in the root + * - recurse into subdirectories to find SKILL.md + */ +export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult { + const { dir, source } = options; + return loadSkillsFromDirInternal(dir, source, true); +} + +function loadSkillsFromDirInternal( + dir: string, + source: string, + includeRootFiles: boolean, + ignoreMatcher?: IgnoreMatcher, + rootDir?: string, +): LoadSkillsResult { + const skills: Skill[] = []; + const diagnostics: ResourceDiagnostic[] = []; + + if (!existsSync(dir)) { + return { skills, diagnostics }; + } + + const root = rootDir ?? dir; + const ig = ignoreMatcher ?? ignore(); + addIgnoreRules(ig, dir, root); + + try { + const entries = readdirSync(dir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.name !== "SKILL.md") { + continue; + } + + const fullPath = join(dir, entry.name); + + let isFile = entry.isFile(); + if (entry.isSymbolicLink()) { + try { + isFile = statSync(fullPath).isFile(); + } catch { + continue; + } + } + + const relPath = toPosixPath(relative(root, fullPath)); + if (!isFile || ig.ignores(relPath)) { + continue; + } + + const result = loadSkillFromFile(fullPath, source); + if (result.skill) { + skills.push(result.skill); + } + diagnostics.push(...result.diagnostics); + return { skills, diagnostics }; + } + + for (const entry of entries) { + if (entry.name.startsWith(".")) { + continue; + } + + // Skip node_modules to avoid scanning dependencies + if (entry.name === "node_modules") { + continue; + } + + const fullPath = join(dir, entry.name); + + // For symlinks, check if they point to a directory and follow them + let isDirectory = entry.isDirectory(); + let isFile = entry.isFile(); + if (entry.isSymbolicLink()) { + try { + const stats = statSync(fullPath); + isDirectory = stats.isDirectory(); + isFile = stats.isFile(); + } catch { + // Broken symlink, skip it + continue; + } + } + + const relPath = toPosixPath(relative(root, fullPath)); + const ignorePath = isDirectory ? `${relPath}/` : relPath; + if (ig.ignores(ignorePath)) { + continue; + } + + if (isDirectory) { + const subResult = loadSkillsFromDirInternal(fullPath, source, false, ig, root); + skills.push(...subResult.skills); + diagnostics.push(...subResult.diagnostics); + continue; + } + + if (!isFile || !includeRootFiles || !entry.name.endsWith(".md")) { + continue; + } + + const result = loadSkillFromFile(fullPath, source); + if (result.skill) { + skills.push(result.skill); + } + diagnostics.push(...result.diagnostics); + } + } catch {} + + return { skills, diagnostics }; +} + +function loadSkillFromFile( + filePath: string, + source: string, +): { skill: Skill | null; diagnostics: ResourceDiagnostic[] } { + const diagnostics: ResourceDiagnostic[] = []; + + try { + const rawContent = readFileSync(filePath, "utf-8"); + const { frontmatter } = parseFrontmatter(rawContent); + const skillDir = dirname(filePath); + const parentDirName = basename(skillDir); + + // Validate description + const descErrors = validateDescription(frontmatter.description); + for (const error of descErrors) { + diagnostics.push({ type: "warning", message: error, path: filePath }); + } + + // Use name from frontmatter, or fall back to parent directory name + const name = frontmatter.name || parentDirName; + + // Validate name + const nameErrors = validateName(name); + for (const error of nameErrors) { + diagnostics.push({ type: "warning", message: error, path: filePath }); + } + + // Still load the skill even with warnings (unless description is completely missing) + if (!frontmatter.description || frontmatter.description.trim() === "") { + return { skill: null, diagnostics }; + } + + return { + skill: { + name, + description: frontmatter.description, + filePath, + baseDir: skillDir, + source, + sourceInfo: createSkillSourceInfo(filePath, skillDir, source), + disableModelInvocation: frontmatter["disable-model-invocation"] === true, + }, + diagnostics, + }; + } catch (error) { + const message = error instanceof Error ? error.message : "failed to parse skill file"; + diagnostics.push({ type: "warning", message, path: filePath }); + return { skill: null, diagnostics }; + } +} + +/** + * Format skills for inclusion in a system prompt. + * Uses XML format per Agent Skills standard. + * See: https://agentskills.io/integrate-skills + * + * Skills with disableModelInvocation=true are excluded from the prompt + * (they can only be invoked explicitly via /skill:name commands). + */ +export function formatSkillsForPrompt(skills: Skill[]): string { + const visibleSkills = skills.filter((s) => !s.disableModelInvocation); + + if (visibleSkills.length === 0) { + return ""; + } + + const lines = [ + "\n\nThe following skills provide specialized instructions for specific tasks.", + "Use the read tool to load a skill's file when the task matches its description.", + "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.", + "", + "", + ]; + + for (const skill of visibleSkills) { + lines.push(" "); + lines.push(` ${escapeXml(skill.name)}`); + lines.push(` ${escapeXml(skill.description)}`); + lines.push(` ${escapeXml(skill.filePath)}`); + lines.push(" "); + } + + lines.push(""); + + return lines.join("\n"); +} + +function escapeXml(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +export interface LoadSkillsOptions { + /** Working directory for project-local skills. */ + cwd: string; + /** Agent config directory for global skills. */ + agentDir: string; + /** Explicit skill paths (files or directories) */ + skillPaths: string[]; + /** Include default skills directories. */ + includeDefaults: boolean; +} + +function normalizePath(input: string): string { + const trimmed = input.trim(); + if (trimmed === "~") { + return homedir(); + } + if (trimmed.startsWith("~/")) { + return join(homedir(), trimmed.slice(2)); + } + if (trimmed.startsWith("~")) { + return join(homedir(), trimmed.slice(1)); + } + return trimmed; +} + +function resolveSkillPath(p: string, cwd: string): string { + const normalized = normalizePath(p); + return isAbsolute(normalized) ? normalized : resolve(cwd, normalized); +} + +/** + * Load skills from all configured locations. + * Returns skills and any validation diagnostics. + */ +export function loadSkills(options: LoadSkillsOptions): LoadSkillsResult { + const { cwd, agentDir, skillPaths, includeDefaults } = options; + + // Resolve agentDir - if not provided, use default from config + const resolvedAgentDir = agentDir ?? getAgentDir(); + + const skillMap = new Map(); + const realPathSet = new Set(); + const allDiagnostics: ResourceDiagnostic[] = []; + const collisionDiagnostics: ResourceDiagnostic[] = []; + + function addSkills(result: LoadSkillsResult) { + allDiagnostics.push(...result.diagnostics); + for (const skill of result.skills) { + // Resolve symlinks to detect duplicate files + const realPath = canonicalizePath(skill.filePath); + + // Skip silently if we've already loaded this exact file (via symlink) + if (realPathSet.has(realPath)) { + continue; + } + + const existing = skillMap.get(skill.name); + if (existing) { + collisionDiagnostics.push({ + type: "collision", + message: `name "${skill.name}" collision`, + path: skill.filePath, + collision: { + resourceType: "skill", + name: skill.name, + winnerPath: existing.filePath, + loserPath: skill.filePath, + }, + }); + } else { + skillMap.set(skill.name, skill); + realPathSet.add(realPath); + } + } + } + + if (includeDefaults) { + addSkills(loadSkillsFromDirInternal(join(resolvedAgentDir, "skills"), "user", true)); + addSkills(loadSkillsFromDirInternal(resolve(cwd, CONFIG_DIR_NAME, "skills"), "project", true)); + } + + const userSkillsDir = join(resolvedAgentDir, "skills"); + const projectSkillsDir = resolve(cwd, CONFIG_DIR_NAME, "skills"); + + const isUnderPath = (target: string, root: string): boolean => { + const normalizedRoot = resolve(root); + if (target === normalizedRoot) { + return true; + } + const prefix = normalizedRoot.endsWith(sep) ? normalizedRoot : `${normalizedRoot}${sep}`; + return target.startsWith(prefix); + }; + + const getSource = (resolvedPath: string): "user" | "project" | "path" => { + if (!includeDefaults) { + if (isUnderPath(resolvedPath, userSkillsDir)) { + return "user"; + } + if (isUnderPath(resolvedPath, projectSkillsDir)) { + return "project"; + } + } + return "path"; + }; + + for (const rawPath of skillPaths) { + const resolvedPath = resolveSkillPath(rawPath, cwd); + if (!existsSync(resolvedPath)) { + allDiagnostics.push({ + type: "warning", + message: "skill path does not exist", + path: resolvedPath, + }); + continue; + } + + try { + const stats = statSync(resolvedPath); + const source = getSource(resolvedPath); + if (stats.isDirectory()) { + addSkills(loadSkillsFromDirInternal(resolvedPath, source, true)); + } else if (stats.isFile() && resolvedPath.endsWith(".md")) { + const result = loadSkillFromFile(resolvedPath, source); + if (result.skill) { + addSkills({ skills: [result.skill], diagnostics: result.diagnostics }); + } else { + allDiagnostics.push(...result.diagnostics); + } + } else { + allDiagnostics.push({ + type: "warning", + message: "skill path is not a markdown file", + path: resolvedPath, + }); + } + } catch (error) { + const message = error instanceof Error ? error.message : "failed to read skill path"; + allDiagnostics.push({ type: "warning", message, path: resolvedPath }); + } + } + + return { + skills: Array.from(skillMap.values()), + diagnostics: [...allDiagnostics, ...collisionDiagnostics], + }; +} diff --git a/src/agents/sessions/slash-commands.ts b/src/agents/sessions/slash-commands.ts new file mode 100644 index 00000000000..4fc450bee30 --- /dev/null +++ b/src/agents/sessions/slash-commands.ts @@ -0,0 +1,40 @@ +import { APP_NAME } from "../config.js"; +import type { SourceInfo } from "./source-info.js"; + +export type SlashCommandSource = "extension" | "prompt" | "skill"; + +export interface SlashCommandInfo { + name: string; + description?: string; + source: SlashCommandSource; + sourceInfo: SourceInfo; +} + +export interface BuiltinSlashCommand { + name: string; + description: string; +} + +export const BUILTIN_SLASH_COMMANDS: ReadonlyArray = [ + { name: "settings", description: "Open settings menu" }, + { name: "model", description: "Select model (opens selector UI)" }, + { name: "scoped-models", description: "Enable/disable models for Ctrl+P cycling" }, + { name: "export", description: "Export session (HTML default, or specify path: .html/.jsonl)" }, + { name: "import", description: "Import and resume a session from a JSONL file" }, + { name: "share", description: "Share session as a secret GitHub gist" }, + { name: "copy", description: "Copy last agent message to clipboard" }, + { name: "name", description: "Set session display name" }, + { name: "session", description: "Show session info and stats" }, + { name: "changelog", description: "Show changelog entries" }, + { name: "hotkeys", description: "Show all keyboard shortcuts" }, + { name: "fork", description: "Create a new fork from a previous user message" }, + { name: "clone", description: "Duplicate the current session at the current position" }, + { name: "tree", description: "Navigate session tree (switch branches)" }, + { name: "login", description: "Configure provider authentication" }, + { name: "logout", description: "Remove provider authentication" }, + { name: "new", description: "Start a new session" }, + { name: "compact", description: "Manually compact the session context" }, + { name: "resume", description: "Resume a different session" }, + { name: "reload", description: "Reload keybindings, extensions, skills, prompts, and themes" }, + { name: "quit", description: `Quit ${APP_NAME}` }, +]; diff --git a/src/agents/sessions/source-info.ts b/src/agents/sessions/source-info.ts new file mode 100644 index 00000000000..3f0a310abef --- /dev/null +++ b/src/agents/sessions/source-info.ts @@ -0,0 +1,40 @@ +import type { PathMetadata } from "./package-manager.js"; + +export type SourceScope = "user" | "project" | "temporary"; +export type SourceOrigin = "package" | "top-level"; + +export interface SourceInfo { + path: string; + source: string; + scope: SourceScope; + origin: SourceOrigin; + baseDir?: string; +} + +export function createSourceInfo(path: string, metadata: PathMetadata): SourceInfo { + return { + path, + source: metadata.source, + scope: metadata.scope, + origin: metadata.origin, + baseDir: metadata.baseDir, + }; +} + +export function createSyntheticSourceInfo( + path: string, + options: { + source: string; + scope?: SourceScope; + origin?: SourceOrigin; + baseDir?: string; + }, +): SourceInfo { + return { + path, + source: options.source, + scope: options.scope ?? "temporary", + origin: options.origin ?? "top-level", + baseDir: options.baseDir, + }; +} diff --git a/src/agents/sessions/system-prompt.ts b/src/agents/sessions/system-prompt.ts new file mode 100644 index 00000000000..af681c1b218 --- /dev/null +++ b/src/agents/sessions/system-prompt.ts @@ -0,0 +1,179 @@ +/** + * System prompt construction and project context loading + */ + +import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js"; +import { formatSkillsForPrompt, type Skill } from "./skills.js"; + +export interface BuildSystemPromptOptions { + /** Custom system prompt (replaces default). */ + customPrompt?: string; + /** Tools to include in prompt. Default: [read, bash, edit, write] */ + selectedTools?: string[]; + /** Optional one-line tool snippets keyed by tool name. */ + toolSnippets?: Record; + /** Additional guideline bullets appended to the default system prompt guidelines. */ + promptGuidelines?: string[]; + /** Text to append to system prompt. */ + appendSystemPrompt?: string; + /** Working directory. */ + cwd: string; + /** Pre-loaded context files. */ + contextFiles?: Array<{ path: string; content: string }>; + /** Pre-loaded skills. */ + skills?: Skill[]; +} + +/** Build the system prompt with tools, guidelines, and context */ +export function buildSystemPrompt(options: BuildSystemPromptOptions): string { + const { + customPrompt, + selectedTools, + toolSnippets, + promptGuidelines, + appendSystemPrompt, + cwd, + contextFiles: providedContextFiles, + skills: providedSkills, + } = options; + const resolvedCwd = cwd; + const promptCwd = resolvedCwd.replace(/\\/g, "/"); + + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, "0"); + const day = String(now.getDate()).padStart(2, "0"); + const date = `${year}-${month}-${day}`; + + const appendSection = appendSystemPrompt ? `\n\n${appendSystemPrompt}` : ""; + + const contextFiles = providedContextFiles ?? []; + const skills = providedSkills ?? []; + + if (customPrompt) { + let prompt = customPrompt; + + if (appendSection) { + prompt += appendSection; + } + + // Append project context files + if (contextFiles.length > 0) { + prompt += "\n\n\n\n"; + prompt += "Project-specific instructions and guidelines:\n\n"; + for (const { path: filePath, content } of contextFiles) { + prompt += `\n${content}\n\n\n`; + } + prompt += "\n"; + } + + // Append skills section (only if read tool is available) + const customPromptHasRead = !selectedTools || selectedTools.includes("read"); + if (customPromptHasRead && skills.length > 0) { + prompt += formatSkillsForPrompt(skills); + } + + // Add date and working directory last + prompt += `\nCurrent date: ${date}`; + prompt += `\nCurrent working directory: ${promptCwd}`; + + return prompt; + } + + // Get absolute paths to documentation and examples + const readmePath = getReadmePath(); + const docsPath = getDocsPath(); + const examplesPath = getExamplesPath(); + + // Build tools list based on selected tools. + // A tool appears in Available tools only when the caller provides a one-line snippet. + const tools = selectedTools || ["read", "bash", "edit", "write"]; + const visibleTools = tools.filter((name) => !!toolSnippets?.[name]); + const toolsList = + visibleTools.length > 0 + ? visibleTools.map((name) => `- ${name}: ${toolSnippets![name]}`).join("\n") + : "(none)"; + + // Build guidelines based on which tools are actually available + const guidelinesList: string[] = []; + const guidelinesSet = new Set(); + const addGuideline = (guideline: string): void => { + if (guidelinesSet.has(guideline)) { + return; + } + guidelinesSet.add(guideline); + guidelinesList.push(guideline); + }; + + const hasBash = tools.includes("bash"); + const hasGrep = tools.includes("grep"); + const hasFind = tools.includes("find"); + const hasLs = tools.includes("ls"); + const hasRead = tools.includes("read"); + + // File exploration guidelines + if (hasBash && !hasGrep && !hasFind && !hasLs) { + addGuideline("Use bash for file operations like ls, rg, find"); + } else if (hasBash && (hasGrep || hasFind || hasLs)) { + addGuideline( + "Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)", + ); + } + + for (const guideline of promptGuidelines ?? []) { + const normalized = guideline.trim(); + if (normalized.length > 0) { + addGuideline(normalized); + } + } + + // Always include these + addGuideline("Be concise in your responses"); + addGuideline("Show file paths clearly when working with files"); + + const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n"); + + let prompt = `You are an expert coding assistant operating inside OpenClaw's embedded coding agent harness. You help users by reading files, executing commands, editing code, and writing new files. + +Available tools: +${toolsList} + +In addition to the tools above, you may have access to other custom tools depending on the project. + +Guidelines: +${guidelines} + +Embedded agent documentation (read only when the user asks about the embedded agent SDK, extensions, themes, skills, or TUI): +- Main documentation: ${readmePath} +- Additional docs: ${docsPath} +- Examples: ${examplesPath} (extensions, custom tools, SDK) +- When reading embedded agent docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory +- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), runtime packages (docs/packages.md) +- When working on embedded agent topics, read the docs and examples, and follow .md cross-references before implementing +- Always read embedded agent .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`; + + if (appendSection) { + prompt += appendSection; + } + + // Append project context files + if (contextFiles.length > 0) { + prompt += "\n\n\n\n"; + prompt += "Project-specific instructions and guidelines:\n\n"; + for (const { path: filePath, content } of contextFiles) { + prompt += `\n${content}\n\n\n`; + } + prompt += "\n"; + } + + // Append skills section (only if read tool is available) + if (hasRead && skills.length > 0) { + prompt += formatSkillsForPrompt(skills); + } + + // Add date and working directory last + prompt += `\nCurrent date: ${date}`; + prompt += `\nCurrent working directory: ${promptCwd}`; + + return prompt; +} diff --git a/src/agents/sessions/telemetry.ts b/src/agents/sessions/telemetry.ts new file mode 100644 index 00000000000..b6c6e3bdab1 --- /dev/null +++ b/src/agents/sessions/telemetry.ts @@ -0,0 +1,17 @@ +import type { SettingsManager } from "./settings-manager.js"; + +function isTruthyEnvFlag(value: string | undefined): boolean { + if (!value) { + return false; + } + return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes"; +} + +export function isInstallTelemetryEnabled( + settingsManager: SettingsManager, + telemetryEnv: string | undefined = process.env.OPENCLAW_TELEMETRY, +): boolean { + return telemetryEnv !== undefined + ? isTruthyEnvFlag(telemetryEnv) + : settingsManager.getEnableInstallTelemetry(); +} diff --git a/src/agents/sessions/timings.ts b/src/agents/sessions/timings.ts new file mode 100644 index 00000000000..8696616a3fa --- /dev/null +++ b/src/agents/sessions/timings.ts @@ -0,0 +1,37 @@ +/** + * Central timing instrumentation for startup profiling. + * Enable with OPENCLAW_TIMING=1 environment variable. + */ + +const ENABLED = process.env.OPENCLAW_TIMING === "1"; +const timings: Array<{ label: string; ms: number }> = []; +let lastTime = Date.now(); + +export function resetTimings(): void { + if (!ENABLED) { + return; + } + timings.length = 0; + lastTime = Date.now(); +} + +export function time(label: string): void { + if (!ENABLED) { + return; + } + const now = Date.now(); + timings.push({ label, ms: now - lastTime }); + lastTime = now; +} + +export function printTimings(): void { + if (!ENABLED || timings.length === 0) { + return; + } + console.error("\n--- Startup Timings ---"); + for (const t of timings) { + console.error(` ${t.label}: ${t.ms}ms`); + } + console.error(` TOTAL: ${timings.reduce((a, b) => a + b.ms, 0)}ms`); + console.error("------------------------\n"); +} diff --git a/src/agents/sessions/tools/bash.ts b/src/agents/sessions/tools/bash.ts new file mode 100644 index 00000000000..3e380119eea --- /dev/null +++ b/src/agents/sessions/tools/bash.ts @@ -0,0 +1,504 @@ +import { spawn } from "node:child_process"; +import { existsSync } from "node:fs"; +import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui"; +import { type Static, Type } from "typebox"; +import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; +import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js"; +import { theme } from "../../modes/interactive/theme/theme.js"; +import type { AgentTool } from "../../runtime/index.js"; +import { waitForChildProcess } from "../../utils/child-process.js"; +import { + getShellConfig, + getShellEnv, + killProcessTree, + trackDetachedChildPid, + untrackDetachedChildPid, +} from "../../utils/shell.js"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; +import { OutputAccumulator } from "./output-accumulator.js"; +import { getTextOutput, invalidArgText, str } from "./render-utils.js"; +import { wrapToolDefinition } from "./tool-definition-wrapper.js"; +import { + DEFAULT_MAX_BYTES, + DEFAULT_MAX_LINES, + formatSize, + type TruncationResult, +} from "./truncate.js"; + +const bashSchema = Type.Object({ + command: Type.String({ description: "Bash command to execute" }), + timeout: Type.Optional( + Type.Number({ description: "Timeout in seconds (optional, no default timeout)" }), + ), +}); + +export type BashToolInput = Static; + +export interface BashToolDetails { + truncation?: TruncationResult; + fullOutputPath?: string; +} + +/** + * Pluggable operations for the bash tool. + * Override these to delegate command execution to remote systems (for example SSH). + */ +export interface BashOperations { + /** + * Execute a command and stream output. + * @param command The command to execute + * @param cwd Working directory + * @param options Execution options + * @returns Promise resolving to exit code (null if killed) + */ + exec: ( + command: string, + cwd: string, + options: { + onData: (data: Buffer) => void; + signal?: AbortSignal; + timeout?: number; + env?: NodeJS.ProcessEnv; + }, + ) => Promise<{ exitCode: number | null }>; +} + +/** + * Create bash operations using OpenClaw runtime's built-in local shell execution backend. + * + * This is useful for extensions that intercept user_bash and still want OpenClaw runtime's + * standard local shell behavior while wrapping or rewriting commands. + */ +export function createLocalBashOperations(options?: { shellPath?: string }): BashOperations { + return { + exec: (command, cwd, { onData, signal, timeout, env }) => { + return new Promise((resolve, reject) => { + const { shell, args } = getShellConfig(options?.shellPath); + if (!existsSync(cwd)) { + reject( + new Error(`Working directory does not exist: ${cwd}\nCannot execute bash commands.`), + ); + return; + } + const child = spawn(shell, [...args, command], { + cwd, + detached: process.platform !== "win32", + env: env ?? getShellEnv(), + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + if (child.pid) { + trackDetachedChildPid(child.pid); + } + let timedOut = false; + let timeoutHandle: NodeJS.Timeout | undefined; + // Set timeout if provided. + if (timeout !== undefined && timeout > 0) { + timeoutHandle = setTimeout(() => { + timedOut = true; + if (child.pid) { + killProcessTree(child.pid); + } + }, timeout * 1000); + } + // Stream stdout and stderr. + child.stdout?.on("data", onData); + child.stderr?.on("data", onData); + // Handle abort signal by killing the entire process tree. + const onAbort = () => { + if (child.pid) { + killProcessTree(child.pid); + } + }; + if (signal) { + if (signal.aborted) { + onAbort(); + } else { + signal.addEventListener("abort", onAbort, { once: true }); + } + } + // Handle shell spawn errors and wait for the process to terminate without hanging + // on inherited stdio handles held by detached descendants. + waitForChildProcess(child) + .then((code) => { + if (child.pid) { + untrackDetachedChildPid(child.pid); + } + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + if (signal) { + signal.removeEventListener("abort", onAbort); + } + if (signal?.aborted) { + reject(new Error("aborted")); + return; + } + if (timedOut) { + reject(new Error(`timeout:${timeout}`)); + return; + } + resolve({ exitCode: code }); + }) + .catch((err) => { + if (child.pid) { + untrackDetachedChildPid(child.pid); + } + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + if (signal) { + signal.removeEventListener("abort", onAbort); + } + reject(err); + }); + }); + }, + }; +} + +export interface BashSpawnContext { + command: string; + cwd: string; + env: NodeJS.ProcessEnv; +} + +export type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext; + +function resolveSpawnContext( + command: string, + cwd: string, + spawnHook?: BashSpawnHook, +): BashSpawnContext { + const baseContext: BashSpawnContext = { command, cwd, env: { ...getShellEnv() } }; + return spawnHook ? spawnHook(baseContext) : baseContext; +} + +export interface BashToolOptions { + /** Custom operations for command execution. Default: local shell */ + operations?: BashOperations; + /** Command prefix prepended to every command (for example shell setup commands) */ + commandPrefix?: string; + /** Optional explicit shell path from settings */ + shellPath?: string; + /** Hook to adjust command, cwd, or env before execution */ + spawnHook?: BashSpawnHook; +} + +const BASH_PREVIEW_LINES = 5; +const BASH_UPDATE_THROTTLE_MS = 100; + +type BashRenderState = { + startedAt: number | undefined; + endedAt: number | undefined; + interval: NodeJS.Timeout | undefined; +}; + +type BashResultRenderState = { + cachedWidth: number | undefined; + cachedLines: string[] | undefined; + cachedSkipped: number | undefined; +}; + +class BashResultRenderComponent extends Container { + state: BashResultRenderState = { + cachedWidth: undefined, + cachedLines: undefined, + cachedSkipped: undefined, + }; +} + +function formatDuration(ms: number): string { + return `${(ms / 1000).toFixed(1)}s`; +} + +function formatBashCall(args: { command?: string; timeout?: number } | undefined): string { + const command = str(args?.command); + const timeout = args?.timeout; + const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : ""; + const commandDisplay = + command === null ? invalidArgText(theme) : command ? command : theme.fg("toolOutput", "..."); + return theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix; +} + +function rebuildBashResultRenderComponent( + component: BashResultRenderComponent, + result: { + content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; + details?: BashToolDetails; + }, + options: ToolRenderResultOptions, + showImages: boolean, + startedAt: number | undefined, + endedAt: number | undefined, +): void { + const state = component.state; + component.clear(); + + let output = getTextOutput(result, showImages).trim(); + const truncation = result.details?.truncation; + const fullOutputPath = result.details?.fullOutputPath; + if (!options.isPartial && truncation?.truncated && fullOutputPath && output.endsWith("]")) { + const footerStart = output.lastIndexOf("\n\n["); + if (footerStart !== -1 && output.slice(footerStart).includes(fullOutputPath)) { + output = output.slice(0, footerStart).trimEnd(); + } + } + + if (output) { + const styledOutput = output + .split("\n") + .map((line) => theme.fg("toolOutput", line)) + .join("\n"); + + if (options.expanded) { + component.addChild(new Text(`\n${styledOutput}`, 0, 0)); + } else { + component.addChild({ + render: (width: number) => { + if (state.cachedLines === undefined || state.cachedWidth !== width) { + const preview = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width); + state.cachedLines = preview.visualLines; + state.cachedSkipped = preview.skippedCount; + state.cachedWidth = width; + } + if (state.cachedSkipped && state.cachedSkipped > 0) { + const hint = + theme.fg("muted", `... (${state.cachedSkipped} earlier lines,`) + + ` ${keyHint("app.tools.expand", "to expand")})`; + return ["", truncateToWidth(hint, width, "..."), ...(state.cachedLines ?? [])]; + } + return ["", ...(state.cachedLines ?? [])]; + }, + invalidate: () => { + state.cachedWidth = undefined; + state.cachedLines = undefined; + state.cachedSkipped = undefined; + }, + }); + } + } + + if (truncation?.truncated || fullOutputPath) { + const warnings: string[] = []; + if (fullOutputPath) { + warnings.push(`Full output: ${fullOutputPath}`); + } + if (truncation?.truncated) { + if (truncation.truncatedBy === "lines") { + warnings.push( + `Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`, + ); + } else { + warnings.push( + `Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`, + ); + } + } + component.addChild(new Text(`\n${theme.fg("warning", `[${warnings.join(". ")}]`)}`, 0, 0)); + } + + if (startedAt !== undefined) { + const label = options.isPartial ? "Elapsed" : "Took"; + const endTime = endedAt ?? Date.now(); + component.addChild( + new Text(`\n${theme.fg("muted", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0), + ); + } +} + +export function createBashToolDefinition( + cwd: string, + options?: BashToolOptions, +): ToolDefinition { + const ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath }); + const commandPrefix = options?.commandPrefix; + const spawnHook = options?.spawnHook; + return { + name: "bash", + label: "bash", + description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`, + promptSnippet: "Execute bash commands (ls, grep, find, etc.)", + parameters: bashSchema, + async execute( + toolCallId, + { command, timeout }: { command: string; timeout?: number }, + signal?: AbortSignal, + onUpdate?, + ctx?, + ) { + void toolCallId; + void ctx; + const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command; + const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook); + const output = new OutputAccumulator({ tempFilePrefix: "openclaw-bash" }); + let updateTimer: NodeJS.Timeout | undefined; + let updateDirty = false; + let lastUpdateAt = 0; + + const emitOutputUpdate = () => { + if (!onUpdate || !updateDirty) { + return; + } + updateDirty = false; + lastUpdateAt = Date.now(); + const snapshot = output.snapshot({ persistIfTruncated: true }); + onUpdate({ + content: [{ type: "text", text: snapshot.content || "" }], + details: { + truncation: snapshot.truncation.truncated ? snapshot.truncation : undefined, + fullOutputPath: snapshot.fullOutputPath, + }, + }); + }; + + const clearUpdateTimer = () => { + if (updateTimer) { + clearTimeout(updateTimer); + updateTimer = undefined; + } + }; + + const scheduleOutputUpdate = () => { + if (!onUpdate) { + return; + } + updateDirty = true; + const delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt); + if (delay <= 0) { + clearUpdateTimer(); + emitOutputUpdate(); + return; + } + updateTimer ??= setTimeout(() => { + updateTimer = undefined; + emitOutputUpdate(); + }, delay); + }; + + if (onUpdate) { + onUpdate({ content: [], details: undefined }); + } + + const handleData = (data: Buffer) => { + output.append(data); + scheduleOutputUpdate(); + }; + + const finishOutput = async () => { + output.finish(); + clearUpdateTimer(); + emitOutputUpdate(); + const snapshot = output.snapshot({ persistIfTruncated: true }); + await output.closeTempFile(); + return snapshot; + }; + + const formatOutput = ( + snapshot: Awaited>, + emptyText = "(no output)", + ) => { + const truncation = snapshot.truncation; + let text = snapshot.content || emptyText; + let details: BashToolDetails | undefined; + if (truncation.truncated) { + details = { truncation, fullOutputPath: snapshot.fullOutputPath }; + const startLine = truncation.totalLines - truncation.outputLines + 1; + const endLine = truncation.totalLines; + if (truncation.lastLinePartial) { + const lastLineSize = formatSize(output.getLastLineBytes()); + text += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`; + } else if (truncation.truncatedBy === "lines") { + text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`; + } else { + text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`; + } + } + return { text, details }; + }; + + const appendStatus = (text: string, status: string) => + `${text ? `${text}\n\n` : ""}${status}`; + + try { + let exitCode: number | null; + try { + const result = await ops.exec(spawnContext.command, spawnContext.cwd, { + onData: handleData, + signal, + timeout, + env: spawnContext.env, + }); + exitCode = result.exitCode; + } catch (err) { + const snapshot = await finishOutput(); + const { text } = formatOutput(snapshot, ""); + if (err instanceof Error && err.message === "aborted") { + throw new Error(appendStatus(text, "Command aborted"), { cause: err }); + } + if (err instanceof Error && err.message.startsWith("timeout:")) { + const timeoutSecs = err.message.split(":")[1]; + throw new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`), { + cause: err, + }); + } + throw err; + } + + const snapshot = await finishOutput(); + const { text: outputText, details } = formatOutput(snapshot); + if (exitCode !== 0 && exitCode !== null) { + throw new Error(appendStatus(outputText, `Command exited with code ${exitCode}`)); + } + return { content: [{ type: "text", text: outputText }], details }; + } finally { + clearUpdateTimer(); + } + }, + renderCall(args, theme, context) { + void theme; + const state = context.state; + if (context.executionStarted && state.startedAt === undefined) { + state.startedAt = Date.now(); + state.endedAt = undefined; + } + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + text.setText(formatBashCall(args)); + return text; + }, + renderResult(result, options, theme, context) { + void theme; + const state = context.state; + if (state.startedAt !== undefined && options.isPartial && !state.interval) { + state.interval = setInterval(() => context.invalidate(), 1000); + } + if (!options.isPartial || context.isError) { + state.endedAt ??= Date.now(); + if (state.interval) { + clearInterval(state.interval); + state.interval = undefined; + } + } + const component = + (context.lastComponent as BashResultRenderComponent | undefined) ?? + new BashResultRenderComponent(); + rebuildBashResultRenderComponent( + component, + result, + options, + context.showImages, + state.startedAt, + state.endedAt, + ); + component.invalidate(); + return component; + }, + }; +} + +export function createBashTool( + cwd: string, + options?: BashToolOptions, +): AgentTool { + return wrapToolDefinition(createBashToolDefinition(cwd, options)); +} diff --git a/src/agents/sessions/tools/edit-diff.ts b/src/agents/sessions/tools/edit-diff.ts new file mode 100644 index 00000000000..c18efcbd9ce --- /dev/null +++ b/src/agents/sessions/tools/edit-diff.ts @@ -0,0 +1,479 @@ +/** + * Shared diff computation utilities for the edit tool. + * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering). + */ + +import { constants } from "node:fs"; +import { access, readFile } from "node:fs/promises"; +import * as Diff from "diff"; +import { resolveToCwd } from "./path-utils.js"; + +export function detectLineEnding(content: string): "\r\n" | "\n" { + const crlfIdx = content.indexOf("\r\n"); + const lfIdx = content.indexOf("\n"); + if (lfIdx === -1) { + return "\n"; + } + if (crlfIdx === -1) { + return "\n"; + } + return crlfIdx < lfIdx ? "\r\n" : "\n"; +} + +export function normalizeToLF(text: string): string { + return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); +} + +export function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string { + return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text; +} + +/** + * Normalize text for fuzzy matching. Applies progressive transformations: + * - Strip trailing whitespace from each line + * - Normalize smart quotes to ASCII equivalents + * - Normalize Unicode dashes/hyphens to ASCII hyphen + * - Normalize special Unicode spaces to regular space + */ +export function normalizeForFuzzyMatch(text: string): string { + return ( + text + .normalize("NFKC") + // Strip trailing whitespace per line + .split("\n") + .map((line) => line.trimEnd()) + .join("\n") + // Smart single quotes → ' + .replace(/[\u2018\u2019\u201A\u201B]/g, "'") + // Smart double quotes → " + .replace(/[\u201C\u201D\u201E\u201F]/g, '"') + // Various dashes/hyphens → - + // U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash, + // U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus + .replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-") + // Special spaces → regular space + // U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP, + // U+205F medium math space, U+3000 ideographic space + .replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ") + ); +} + +export interface FuzzyMatchResult { + /** Whether a match was found */ + found: boolean; + /** The index where the match starts (in the content that should be used for replacement) */ + index: number; + /** Length of the matched text */ + matchLength: number; + /** Whether fuzzy matching was used (false = exact match) */ + usedFuzzyMatch: boolean; + /** + * The content to use for replacement operations. + * When exact match: original content. When fuzzy match: normalized content. + */ + contentForReplacement: string; +} + +export interface Edit { + oldText: string; + newText: string; +} + +interface MatchedEdit { + editIndex: number; + matchIndex: number; + matchLength: number; + newText: string; +} + +export interface AppliedEditsResult { + baseContent: string; + newContent: string; +} + +/** + * Find oldText in content, trying exact match first, then fuzzy match. + * When fuzzy matching is used, the returned contentForReplacement is the + * fuzzy-normalized version of the content (trailing whitespace stripped, + * Unicode quotes/dashes normalized to ASCII). + */ +export function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult { + // Try exact match first + const exactIndex = content.indexOf(oldText); + if (exactIndex !== -1) { + return { + found: true, + index: exactIndex, + matchLength: oldText.length, + usedFuzzyMatch: false, + contentForReplacement: content, + }; + } + + // Try fuzzy match - work entirely in normalized space + const fuzzyContent = normalizeForFuzzyMatch(content); + const fuzzyOldText = normalizeForFuzzyMatch(oldText); + const fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText); + + if (fuzzyIndex === -1) { + return { + found: false, + index: -1, + matchLength: 0, + usedFuzzyMatch: false, + contentForReplacement: content, + }; + } + + // When fuzzy matching, we work in the normalized space for replacement. + // This means the output will have normalized whitespace/quotes/dashes, + // which is acceptable since we're fixing minor formatting differences anyway. + return { + found: true, + index: fuzzyIndex, + matchLength: fuzzyOldText.length, + usedFuzzyMatch: true, + contentForReplacement: fuzzyContent, + }; +} + +/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */ +export function stripBom(content: string): { bom: string; text: string } { + return content.startsWith("\uFEFF") + ? { bom: "\uFEFF", text: content.slice(1) } + : { bom: "", text: content }; +} + +function countOccurrences(content: string, oldText: string): number { + const fuzzyContent = normalizeForFuzzyMatch(content); + const fuzzyOldText = normalizeForFuzzyMatch(oldText); + return fuzzyContent.split(fuzzyOldText).length - 1; +} + +function getNotFoundError(path: string, editIndex: number, totalEdits: number): Error { + if (totalEdits === 1) { + return new Error( + `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`, + ); + } + return new Error( + `Could not find edits[${editIndex}] in ${path}. The oldText must match exactly including all whitespace and newlines.`, + ); +} + +function getDuplicateError( + path: string, + editIndex: number, + totalEdits: number, + occurrences: number, +): Error { + if (totalEdits === 1) { + return new Error( + `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`, + ); + } + return new Error( + `Found ${occurrences} occurrences of edits[${editIndex}] in ${path}. Each oldText must be unique. Please provide more context to make it unique.`, + ); +} + +function getEmptyOldTextError(path: string, editIndex: number, totalEdits: number): Error { + if (totalEdits === 1) { + return new Error(`oldText must not be empty in ${path}.`); + } + return new Error(`edits[${editIndex}].oldText must not be empty in ${path}.`); +} + +function getNoChangeError(path: string, totalEdits: number): Error { + if (totalEdits === 1) { + return new Error( + `No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`, + ); + } + return new Error(`No changes made to ${path}. The replacements produced identical content.`); +} + +/** + * Apply one or more exact-text replacements to LF-normalized content. + * + * All edits are matched against the same original content. Replacements are + * then applied in reverse order so offsets remain stable. If any edit needs + * fuzzy matching, the operation runs in fuzzy-normalized content space to + * preserve current single-edit behavior. + */ +export function applyEditsToNormalizedContent( + normalizedContent: string, + edits: Edit[], + path: string, +): AppliedEditsResult { + const normalizedEdits = edits.map((edit) => ({ + oldText: normalizeToLF(edit.oldText), + newText: normalizeToLF(edit.newText), + })); + + for (let i = 0; i < normalizedEdits.length; i++) { + if (normalizedEdits[i].oldText.length === 0) { + throw getEmptyOldTextError(path, i, normalizedEdits.length); + } + } + + const initialMatches = normalizedEdits.map((edit) => + fuzzyFindText(normalizedContent, edit.oldText), + ); + const baseContent = initialMatches.some((match) => match.usedFuzzyMatch) + ? normalizeForFuzzyMatch(normalizedContent) + : normalizedContent; + + const matchedEdits: MatchedEdit[] = []; + for (let i = 0; i < normalizedEdits.length; i++) { + const edit = normalizedEdits[i]; + const matchResult = fuzzyFindText(baseContent, edit.oldText); + if (!matchResult.found) { + throw getNotFoundError(path, i, normalizedEdits.length); + } + + const occurrences = countOccurrences(baseContent, edit.oldText); + if (occurrences > 1) { + throw getDuplicateError(path, i, normalizedEdits.length, occurrences); + } + + matchedEdits.push({ + editIndex: i, + matchIndex: matchResult.index, + matchLength: matchResult.matchLength, + newText: edit.newText, + }); + } + + matchedEdits.sort((a, b) => a.matchIndex - b.matchIndex); + for (let i = 1; i < matchedEdits.length; i++) { + const previous = matchedEdits[i - 1]; + const current = matchedEdits[i]; + if (previous.matchIndex + previous.matchLength > current.matchIndex) { + throw new Error( + `edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${path}. Merge them into one edit or target disjoint regions.`, + ); + } + } + + let newContent = baseContent; + for (let i = matchedEdits.length - 1; i >= 0; i--) { + const edit = matchedEdits[i]; + newContent = + newContent.slice(0, edit.matchIndex) + + edit.newText + + newContent.slice(edit.matchIndex + edit.matchLength); + } + + if (baseContent === newContent) { + throw getNoChangeError(path, normalizedEdits.length); + } + + return { baseContent, newContent }; +} + +/** Generate a standard unified patch. */ +export function generateUnifiedPatch( + path: string, + oldContent: string, + newContent: string, + contextLines = 4, +): string { + return Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, { + context: contextLines, + headerOptions: Diff.FILE_HEADERS_ONLY, + }); +} + +/** + * Generate a display-oriented diff string with line numbers and context. + * Returns both the diff string and the first changed line number (in the new file). + */ +export function generateDiffString( + oldContent: string, + newContent: string, + contextLines = 4, +): { diff: string; firstChangedLine: number | undefined } { + const parts = Diff.diffLines(oldContent, newContent); + const output: string[] = []; + + const oldLines = oldContent.split("\n"); + const newLines = newContent.split("\n"); + const maxLineNum = Math.max(oldLines.length, newLines.length); + const lineNumWidth = String(maxLineNum).length; + + let oldLineNum = 1; + let newLineNum = 1; + let lastWasChange = false; + let firstChangedLine: number | undefined; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const raw = part.value.split("\n"); + if (raw[raw.length - 1] === "") { + raw.pop(); + } + + if (part.added || part.removed) { + // Capture the first changed line (in the new file) + if (firstChangedLine === undefined) { + firstChangedLine = newLineNum; + } + + // Show the change + for (const line of raw) { + if (part.added) { + const lineNum = String(newLineNum).padStart(lineNumWidth, " "); + output.push(`+${lineNum} ${line}`); + newLineNum++; + } else { + // removed + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(`-${lineNum} ${line}`); + oldLineNum++; + } + } + lastWasChange = true; + } else { + // Context lines - only show a few before/after changes + const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed); + const hasLeadingChange = lastWasChange; + const hasTrailingChange = nextPartIsChange; + + if (hasLeadingChange && hasTrailingChange) { + if (raw.length <= contextLines * 2) { + for (const line of raw) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + } else { + const leadingLines = raw.slice(0, contextLines); + const trailingLines = raw.slice(raw.length - contextLines); + const skippedLines = raw.length - leadingLines.length - trailingLines.length; + + for (const line of leadingLines) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + + output.push(` ${"".padStart(lineNumWidth, " ")} ...`); + oldLineNum += skippedLines; + newLineNum += skippedLines; + + for (const line of trailingLines) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + } + } else if (hasLeadingChange) { + const shownLines = raw.slice(0, contextLines); + const skippedLines = raw.length - shownLines.length; + + for (const line of shownLines) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + + if (skippedLines > 0) { + output.push(` ${"".padStart(lineNumWidth, " ")} ...`); + oldLineNum += skippedLines; + newLineNum += skippedLines; + } + } else if (hasTrailingChange) { + const skippedLines = Math.max(0, raw.length - contextLines); + if (skippedLines > 0) { + output.push(` ${"".padStart(lineNumWidth, " ")} ...`); + oldLineNum += skippedLines; + newLineNum += skippedLines; + } + + for (const line of raw.slice(skippedLines)) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + } else { + // Skip these context lines entirely + oldLineNum += raw.length; + newLineNum += raw.length; + } + + lastWasChange = false; + } + } + + return { diff: output.join("\n"), firstChangedLine }; +} + +export interface EditDiffResult { + diff: string; + firstChangedLine: number | undefined; +} + +export interface EditDiffError { + error: string; +} + +/** + * Compute the diff for one or more edit operations without applying them. + * Used for preview rendering in the TUI before the tool executes. + */ +export async function computeEditsDiff( + path: string, + edits: Edit[], + cwd: string, +): Promise { + const absolutePath = resolveToCwd(path, cwd); + + try { + // Check if file exists and is readable + try { + await access(absolutePath, constants.R_OK); + } catch (error: unknown) { + const errorMessage = + error instanceof Error && "code" in error + ? `Error code: ${String(error.code)}` + : String(error); + return { error: `Could not edit file: ${path}. ${errorMessage}.` }; + } + + // Read the file + const rawContent = await readFile(absolutePath, "utf-8"); + + // Strip BOM before matching (LLM won't include invisible BOM in oldText) + const { text: content } = stripBom(rawContent); + const normalizedContent = normalizeToLF(content); + const { baseContent, newContent } = applyEditsToNormalizedContent( + normalizedContent, + edits, + path, + ); + + // Generate the diff + return generateDiffString(baseContent, newContent); + } catch (err) { + return { error: err instanceof Error ? err.message : String(err) }; + } +} + +/** + * Compute the diff for a single edit operation without applying it. + * Kept as a convenience wrapper for single-edit callers. + */ +export async function computeEditDiff( + path: string, + oldText: string, + newText: string, + cwd: string, +): Promise { + return computeEditsDiff(path, [{ oldText, newText }], cwd); +} diff --git a/src/agents/sessions/tools/edit.ts b/src/agents/sessions/tools/edit.ts new file mode 100644 index 00000000000..075c6dd22d7 --- /dev/null +++ b/src/agents/sessions/tools/edit.ts @@ -0,0 +1,542 @@ +import { constants } from "node:fs"; +import { + access as fsAccess, + readFile as fsReadFile, + writeFile as fsWriteFile, +} from "node:fs/promises"; +import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui"; +import { type Static, Type } from "typebox"; +import { renderDiff } from "../../modes/interactive/components/diff.js"; +import type { AgentTool } from "../../runtime/index.js"; +import type { ToolDefinition } from "../extensions/types.js"; +import { + applyEditsToNormalizedContent, + computeEditsDiff, + detectLineEnding, + type Edit, + type EditDiffError, + type EditDiffResult, + generateDiffString, + generateUnifiedPatch, + normalizeToLF, + restoreLineEndings, + stripBom, +} from "./edit-diff.js"; +import { withFileMutationQueue } from "./file-mutation-queue.js"; +import { resolveToCwd } from "./path-utils.js"; +import { invalidArgText, shortenPath, str } from "./render-utils.js"; +import { wrapToolDefinition } from "./tool-definition-wrapper.js"; + +type EditPreview = EditDiffResult | EditDiffError; + +type EditRenderState = { + callComponent?: EditCallRenderComponent; +}; + +const replaceEditSchema = Type.Object( + { + oldText: Type.String({ + description: + "Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call.", + }), + newText: Type.String({ description: "Replacement text for this targeted edit." }), + }, + { additionalProperties: false }, +); + +const editSchema = Type.Object( + { + path: Type.String({ description: "Path to the file to edit (relative or absolute)" }), + edits: Type.Array(replaceEditSchema, { + description: + "One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead.", + }), + }, + { additionalProperties: false }, +); + +export type EditToolInput = Static; +type LegacyEditToolInput = Record & { + edits?: unknown; + oldText?: unknown; + newText?: unknown; +}; + +export interface EditToolDetails { + /** Display-oriented diff of the changes made */ + diff: string; + /** Standard unified patch of the changes made */ + patch: string; + /** Line number of the first change in the new file (for editor navigation) */ + firstChangedLine?: number; +} + +/** + * Pluggable operations for the edit tool. + * Override these to delegate file editing to remote systems (for example SSH). + */ +export interface EditOperations { + /** Read file contents as a Buffer */ + readFile: (absolutePath: string) => Promise; + /** Write content to a file */ + writeFile: (absolutePath: string, content: string) => Promise; + /** Check if file is readable and writable (throw if not) */ + access: (absolutePath: string) => Promise; +} + +const defaultEditOperations: EditOperations = { + readFile: (path) => fsReadFile(path), + writeFile: (path, content) => fsWriteFile(path, content, "utf-8"), + access: (path) => fsAccess(path, constants.R_OK | constants.W_OK), +}; + +export interface EditToolOptions { + /** Custom operations for file editing. Default: local filesystem */ + operations?: EditOperations; +} + +function prepareEditArguments(input: unknown): EditToolInput { + if (!input || typeof input !== "object") { + return input as EditToolInput; + } + + const args = input as Record; + + // Some models (Opus 4.6, GLM-5.1) send edits as a JSON string instead of an array + if (typeof args.edits === "string") { + try { + const parsed = JSON.parse(args.edits); + if (Array.isArray(parsed)) { + args.edits = parsed; + } + } catch {} + } + + const legacy = args as LegacyEditToolInput; + if (typeof legacy.oldText !== "string" || typeof legacy.newText !== "string") { + return args as EditToolInput; + } + + const edits = Array.isArray(legacy.edits) ? [...legacy.edits] : []; + edits.push({ oldText: legacy.oldText, newText: legacy.newText }); + const { oldText, newText, ...rest } = legacy; + return { ...rest, edits } as EditToolInput; +} + +function validateEditInput(input: EditToolInput): { path: string; edits: Edit[] } { + if (!Array.isArray(input.edits) || input.edits.length === 0) { + throw new Error("Edit tool input is invalid. edits must contain at least one replacement."); + } + return { path: input.path, edits: input.edits }; +} + +type RenderableEditArgs = { + path?: string; + file_path?: string; + edits?: Edit[]; + oldText?: string; + newText?: string; +}; + +type EditToolResultLike = { + content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; + details?: EditToolDetails; +}; + +type EditCallRenderComponent = Box & { + preview?: EditPreview; + previewArgsKey?: string; + previewPending?: boolean; + settledError?: boolean; +}; + +function createEditCallRenderComponent(): EditCallRenderComponent { + return Object.assign(new Box(1, 1, (text: string) => text), { + preview: undefined as EditPreview | undefined, + previewArgsKey: undefined as string | undefined, + previewPending: false, + settledError: false, + }); +} + +function getEditCallRenderComponent( + state: EditRenderState, + lastComponent: unknown, +): EditCallRenderComponent { + if (lastComponent instanceof Box) { + const component = lastComponent as EditCallRenderComponent; + state.callComponent = component; + return component; + } + if (state.callComponent) { + return state.callComponent; + } + const component = createEditCallRenderComponent(); + state.callComponent = component; + return component; +} + +function getRenderablePreviewInput( + args: RenderableEditArgs | undefined, +): { path: string; edits: Edit[] } | null { + if (!args) { + return null; + } + + const path = + typeof args.path === "string" + ? args.path + : typeof args.file_path === "string" + ? args.file_path + : null; + if (!path) { + return null; + } + + if ( + Array.isArray(args.edits) && + args.edits.length > 0 && + args.edits.every( + (edit) => typeof edit?.oldText === "string" && typeof edit?.newText === "string", + ) + ) { + return { path, edits: args.edits }; + } + + if (typeof args.oldText === "string" && typeof args.newText === "string") { + return { path, edits: [{ oldText: args.oldText, newText: args.newText }] }; + } + + return null; +} + +function formatEditCall( + args: RenderableEditArgs | undefined, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, +): string { + const invalidArg = invalidArgText(theme); + const rawPath = str(args?.file_path ?? args?.path); + const path = rawPath !== null ? shortenPath(rawPath) : null; + const pathDisplay = + path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."); + return `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`; +} + +function formatEditResult( + args: RenderableEditArgs | undefined, + preview: EditPreview | undefined, + result: EditToolResultLike, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, + isError: boolean, +): string | undefined { + const rawPath = str(args?.file_path ?? args?.path); + const previewDiff = preview && !("error" in preview) ? preview.diff : undefined; + const previewError = preview && "error" in preview ? preview.error : undefined; + if (isError) { + const errorText = result.content + .filter((c) => c.type === "text") + .map((c) => c.text || "") + .join("\n"); + if (!errorText || errorText === previewError) { + return undefined; + } + return theme.fg("error", errorText); + } + + const resultDiff = result.details?.diff; + if (resultDiff && resultDiff !== previewDiff) { + return renderDiff(resultDiff, { filePath: rawPath ?? undefined }); + } + + return undefined; +} + +function getEditHeaderBg( + preview: EditPreview | undefined, + settledError: boolean | undefined, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, +): (text: string) => string { + if (preview) { + if ("error" in preview) { + return (text: string) => theme.bg("toolErrorBg", text); + } + return (text: string) => theme.bg("toolSuccessBg", text); + } + if (settledError) { + return (text: string) => theme.bg("toolErrorBg", text); + } + return (text: string) => theme.bg("toolPendingBg", text); +} + +function buildEditCallComponent( + component: EditCallRenderComponent, + args: RenderableEditArgs | undefined, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, +): EditCallRenderComponent { + component.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme)); + component.clear(); + component.addChild(new Text(formatEditCall(args, theme), 0, 0)); + + if (!component.preview) { + return component; + } + + const body = + "error" in component.preview + ? theme.fg("error", component.preview.error) + : renderDiff(component.preview.diff); + component.addChild(new Spacer(1)); + component.addChild(new Text(body, 0, 0)); + return component; +} + +function setEditPreview( + component: EditCallRenderComponent, + preview: EditPreview, + argsKey: string | undefined, +): boolean { + const current = component.preview; + const changed = + current === undefined || + ("error" in current && "error" in preview + ? current.error !== preview.error + : "error" in current !== "error" in preview) || + (!("error" in current) && + !("error" in preview) && + (current.diff !== preview.diff || current.firstChangedLine !== preview.firstChangedLine)); + component.preview = preview; + component.previewArgsKey = argsKey; + component.previewPending = false; + return changed; +} + +export function createEditToolDefinition( + cwd: string, + options?: EditToolOptions, +): ToolDefinition { + const ops = options?.operations ?? defaultEditOperations; + return { + name: "edit", + label: "edit", + description: + "Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.", + promptSnippet: + "Make precise file edits with exact text replacement, including multiple disjoint edits in one call", + promptGuidelines: [ + "Use edit for precise changes (edits[].oldText must match exactly)", + "When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls", + "Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.", + "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.", + ], + parameters: editSchema, + renderShell: "self", + prepareArguments: prepareEditArguments, + async execute(toolCallId, input: EditToolInput, signal?: AbortSignal, onUpdate?, ctx?) { + void toolCallId; + void onUpdate; + void ctx; + const { path, edits } = validateEditInput(input); + const absolutePath = resolveToCwd(path, cwd); + + return withFileMutationQueue( + absolutePath, + () => + new Promise<{ + content: Array<{ type: "text"; text: string }>; + details: EditToolDetails | undefined; + }>((resolve, reject) => { + // Check if already aborted. + if (signal?.aborted) { + reject(new Error("Operation aborted")); + return; + } + + let aborted = false; + + // Set up abort handler. + const onAbort = () => { + aborted = true; + reject(new Error("Operation aborted")); + }; + + if (signal) { + signal.addEventListener("abort", onAbort, { once: true }); + } + + // Perform the edit operation. + void (async () => { + try { + // Check if file exists. + try { + await ops.access(absolutePath); + } catch (error: unknown) { + const errorMessage = + error instanceof Error && "code" in error + ? `Error code: ${String(error.code)}` + : String(error); + if (signal) { + signal.removeEventListener("abort", onAbort); + } + reject(new Error(`Could not edit file: ${path}. ${errorMessage}.`)); + return; + } + + // Check if aborted before reading. + if (aborted) { + return; + } + + // Read the file. + const buffer = await ops.readFile(absolutePath); + const rawContent = buffer.toString("utf-8"); + + // Check if aborted after reading. + if (aborted) { + return; + } + + // Strip BOM before matching. The model will not include an invisible BOM in oldText. + const { bom, text: content } = stripBom(rawContent); + const originalEnding = detectLineEnding(content); + const normalizedContent = normalizeToLF(content); + const { baseContent, newContent } = applyEditsToNormalizedContent( + normalizedContent, + edits, + path, + ); + + // Check if aborted before writing. + if (aborted) { + return; + } + + const finalContent = bom + restoreLineEndings(newContent, originalEnding); + await ops.writeFile(absolutePath, finalContent); + + // Check if aborted after writing. + if (aborted) { + return; + } + + // Clean up abort handler. + if (signal) { + signal.removeEventListener("abort", onAbort); + } + + const diffResult = generateDiffString(baseContent, newContent); + const patch = generateUnifiedPatch(path, baseContent, newContent); + resolve({ + content: [ + { + type: "text", + text: `Successfully replaced ${edits.length} block(s) in ${path}.`, + }, + ], + details: { + diff: diffResult.diff, + patch, + firstChangedLine: diffResult.firstChangedLine, + }, + }); + } catch (error: unknown) { + // Clean up abort handler. + if (signal) { + signal.removeEventListener("abort", onAbort); + } + + if (!aborted) { + reject(error instanceof Error ? error : new Error(String(error))); + } + } + })(); + }), + ); + }, + renderCall(args, theme, context) { + const component = getEditCallRenderComponent(context.state, context.lastComponent); + const previewInput = getRenderablePreviewInput(args as RenderableEditArgs | undefined); + const argsKey = previewInput + ? JSON.stringify({ path: previewInput.path, edits: previewInput.edits }) + : undefined; + + if (component.previewArgsKey !== argsKey) { + component.preview = undefined; + component.previewArgsKey = argsKey; + component.previewPending = false; + component.settledError = false; + } + + if (context.argsComplete && previewInput && !component.preview && !component.previewPending) { + component.previewPending = true; + const requestKey = argsKey; + void computeEditsDiff(previewInput.path, previewInput.edits, context.cwd).then( + (preview) => { + if (component.previewArgsKey === requestKey) { + setEditPreview(component, preview, requestKey); + context.invalidate(); + } + }, + ); + } + + return buildEditCallComponent(component, args, theme); + }, + renderResult(result, options, theme, context) { + void options; + const callComponent = context.state.callComponent; + const previewInput = getRenderablePreviewInput( + context.args as RenderableEditArgs | undefined, + ); + const argsKey = previewInput + ? JSON.stringify({ path: previewInput.path, edits: previewInput.edits }) + : undefined; + const typedResult = result as EditToolResultLike; + const resultDiff = !context.isError ? typedResult.details?.diff : undefined; + let changed = false; + if (callComponent) { + if (typeof resultDiff === "string") { + changed = + setEditPreview( + callComponent, + { diff: resultDiff, firstChangedLine: typedResult.details?.firstChangedLine }, + argsKey, + ) || changed; + } + if (callComponent.settledError !== context.isError) { + callComponent.settledError = context.isError; + changed = true; + } + if (changed) { + buildEditCallComponent( + callComponent, + context.args as RenderableEditArgs | undefined, + theme, + ); + } + } + + const output = formatEditResult( + context.args, + callComponent?.preview, + typedResult, + theme, + context.isError, + ); + const component = (context.lastComponent as Container | undefined) ?? new Container(); + component.clear(); + if (!output) { + return component; + } + component.addChild(new Spacer(1)); + component.addChild(new Text(output, 1, 0)); + return component; + }, + }; +} + +export function createEditTool( + cwd: string, + options?: EditToolOptions, +): AgentTool { + return wrapToolDefinition(createEditToolDefinition(cwd, options)); +} diff --git a/src/agents/sessions/tools/file-mutation-queue.ts b/src/agents/sessions/tools/file-mutation-queue.ts new file mode 100644 index 00000000000..4eba8eb0766 --- /dev/null +++ b/src/agents/sessions/tools/file-mutation-queue.ts @@ -0,0 +1,39 @@ +import { realpathSync } from "node:fs"; +import { resolve } from "node:path"; + +const fileMutationQueues = new Map>(); + +function getMutationQueueKey(filePath: string): string { + const resolvedPath = resolve(filePath); + try { + return realpathSync.native(resolvedPath); + } catch { + return resolvedPath; + } +} + +/** + * Serialize file mutation operations targeting the same file. + * Operations for different files still run in parallel. + */ +export async function withFileMutationQueue(filePath: string, fn: () => Promise): Promise { + const key = getMutationQueueKey(filePath); + const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve(); + + let releaseNext!: () => void; + const nextQueue = new Promise((resolveQueue) => { + releaseNext = resolveQueue; + }); + const chainedQueue = currentQueue.then(() => nextQueue); + fileMutationQueues.set(key, chainedQueue); + + await currentQueue; + try { + return await fn(); + } finally { + releaseNext(); + if (fileMutationQueues.get(key) === chainedQueue) { + fileMutationQueues.delete(key); + } + } +} diff --git a/src/agents/sessions/tools/find.ts b/src/agents/sessions/tools/find.ts new file mode 100644 index 00000000000..2464a9ecd0d --- /dev/null +++ b/src/agents/sessions/tools/find.ts @@ -0,0 +1,394 @@ +import { spawn } from "node:child_process"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { createInterface } from "node:readline"; +import { Text } from "@earendil-works/pi-tui"; +import { type Static, Type } from "typebox"; +import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; +import type { AgentTool } from "../../runtime/index.js"; +import { ensureTool } from "../../utils/tools-manager.js"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; +import { resolveToCwd } from "./path-utils.js"; +import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js"; +import { wrapToolDefinition } from "./tool-definition-wrapper.js"; +import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js"; + +function toPosixPath(value: string): string { + return value.split(path.sep).join("/"); +} + +const findSchema = Type.Object({ + pattern: Type.String({ + description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'", + }), + path: Type.Optional( + Type.String({ description: "Directory to search in (default: current directory)" }), + ), + limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 1000)" })), +}); + +export type FindToolInput = Static; + +const DEFAULT_LIMIT = 1000; + +export interface FindToolDetails { + truncation?: TruncationResult; + resultLimitReached?: number; +} + +/** + * Pluggable operations for the find tool. + * Override these to delegate file search to remote systems (for example SSH). + */ +export interface FindOperations { + /** Check if path exists */ + exists: (absolutePath: string) => Promise | boolean; + /** Find files matching glob pattern. Returns relative or absolute paths. */ + glob: ( + pattern: string, + cwd: string, + options: { ignore: string[]; limit: number }, + ) => Promise | string[]; +} + +const defaultFindOperations: FindOperations = { + exists: existsSync, + // This is a placeholder. Actual fd execution happens in execute() when no custom glob is provided. + glob: () => [], +}; + +export interface FindToolOptions { + /** Custom operations for find. Default: local filesystem plus fd */ + operations?: FindOperations; +} + +function formatFindCall( + args: { pattern: string; path?: string; limit?: number } | undefined, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, +): string { + const pattern = str(args?.pattern); + const rawPath = str(args?.path); + const path = rawPath !== null ? shortenPath(rawPath || ".") : null; + const limit = args?.limit; + const invalidArg = invalidArgText(theme); + let text = + theme.fg("toolTitle", theme.bold("find")) + + " " + + (pattern === null ? invalidArg : theme.fg("accent", pattern || "")) + + theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`); + if (limit !== undefined) { + text += theme.fg("toolOutput", ` (limit ${limit})`); + } + return text; +} + +function formatFindResult( + result: { + content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; + details?: FindToolDetails; + }, + options: ToolRenderResultOptions, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, + showImages: boolean, +): string { + const output = getTextOutput(result, showImages).trim(); + let text = ""; + if (output) { + const lines = output.split("\n"); + const maxLines = options.expanded ? lines.length : 20; + const displayLines = lines.slice(0, maxLines); + const remaining = lines.length - maxLines; + text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`; + if (remaining > 0) { + text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`; + } + } + + const resultLimit = result.details?.resultLimitReached; + const truncation = result.details?.truncation; + if (resultLimit || truncation?.truncated) { + const warnings: string[] = []; + if (resultLimit) { + warnings.push(`${resultLimit} results limit`); + } + if (truncation?.truncated) { + warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`); + } + text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`; + } + return text; +} + +export function createFindToolDefinition( + cwd: string, + options?: FindToolOptions, +): ToolDefinition { + const customOps = options?.operations; + return { + name: "find", + label: "find", + description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`, + promptSnippet: "Find files by glob pattern (respects .gitignore)", + parameters: findSchema, + async execute( + toolCallId, + { pattern, path: searchDir, limit }: { pattern: string; path?: string; limit?: number }, + signal?: AbortSignal, + onUpdate?, + ctx?, + ) { + void toolCallId; + void onUpdate; + void ctx; + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(new Error("Operation aborted")); + return; + } + + let settled = false; + let stopChild: (() => void) | undefined; + const settle = (fn: () => void) => { + if (settled) { + return; + } + settled = true; + signal?.removeEventListener("abort", onAbort); + stopChild = undefined; + fn(); + }; + const onAbort = () => { + stopChild?.(); + settle(() => reject(new Error("Operation aborted"))); + }; + signal?.addEventListener("abort", onAbort, { once: true }); + + void (async () => { + try { + const searchPath = resolveToCwd(searchDir || ".", cwd); + const effectiveLimit = limit ?? DEFAULT_LIMIT; + const ops = customOps ?? defaultFindOperations; + + // If custom operations provide glob(), use that instead of fd. + if (customOps?.glob) { + if (!(await ops.exists(searchPath))) { + settle(() => reject(new Error(`Path not found: ${searchPath}`))); + return; + } + if (signal?.aborted) { + settle(() => reject(new Error("Operation aborted"))); + return; + } + const results = await ops.glob(pattern, searchPath, { + ignore: ["**/node_modules/**", "**/.git/**"], + limit: effectiveLimit, + }); + if (signal?.aborted) { + settle(() => reject(new Error("Operation aborted"))); + return; + } + if (results.length === 0) { + settle(() => + resolve({ + content: [{ type: "text", text: "No files found matching pattern" }], + details: undefined, + }), + ); + return; + } + + // Relativize paths against the search root for stable output. + const relativized = results.map((p) => { + if (p.startsWith(searchPath)) { + return toPosixPath(p.slice(searchPath.length + 1)); + } + return toPosixPath(path.relative(searchPath, p)); + }); + const resultLimitReached = relativized.length >= effectiveLimit; + const rawOutput = relativized.join("\n"); + const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); + let resultOutput = truncation.content; + const details: FindToolDetails = {}; + const notices: string[] = []; + if (resultLimitReached) { + notices.push(`${effectiveLimit} results limit reached`); + details.resultLimitReached = effectiveLimit; + } + if (truncation.truncated) { + notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); + details.truncation = truncation; + } + if (notices.length > 0) { + resultOutput += `\n\n[${notices.join(". ")}]`; + } + settle(() => + resolve({ + content: [{ type: "text", text: resultOutput }], + details: Object.keys(details).length > 0 ? details : undefined, + }), + ); + return; + } + + // Default implementation uses fd. + const fdPath = await ensureTool("fd", true); + if (signal?.aborted) { + settle(() => reject(new Error("Operation aborted"))); + return; + } + if (!fdPath) { + settle(() => reject(new Error("fd is not available and could not be downloaded"))); + return; + } + + // Build fd arguments. --no-require-git makes fd apply hierarchical .gitignore + // semantics whether or not the search path is inside a git repository, without + // leaking sibling-directory rules the way --ignore-file (a global source) would. + const args: string[] = [ + "--glob", + "--color=never", + "--hidden", + "--no-require-git", + "--max-results", + String(effectiveLimit), + ]; + + // fd --glob matches against the basename unless --full-path is set; in --full-path + // mode it matches against the absolute candidate path, so a path-containing + // pattern like 'src/**/*.spec.ts' needs a leading '**/' to match anything. + let effectivePattern = pattern; + if (pattern.includes("/")) { + args.push("--full-path"); + if (!pattern.startsWith("/") && !pattern.startsWith("**/") && pattern !== "**") { + effectivePattern = `**/${pattern}`; + } + } + args.push("--", effectivePattern, searchPath); + + const child = spawn(fdPath, args, { stdio: ["ignore", "pipe", "pipe"] }); + const rl = createInterface({ input: child.stdout }); + let stderr = ""; + const lines: string[] = []; + + stopChild = () => { + if (!child.killed) { + child.kill(); + } + }; + + const cleanup = () => { + rl.close(); + }; + + child.stderr?.on("data", (chunk) => { + stderr += chunk.toString(); + }); + + rl.on("line", (line) => { + lines.push(line); + }); + + child.on("error", (error) => { + cleanup(); + settle(() => reject(new Error(`Failed to run fd: ${error.message}`))); + }); + + child.on("close", (code) => { + cleanup(); + if (signal?.aborted) { + settle(() => reject(new Error("Operation aborted"))); + return; + } + const output = lines.join("\n"); + if (code !== 0) { + const errorMsg = stderr.trim() || `fd exited with code ${code}`; + if (!output) { + settle(() => reject(new Error(errorMsg))); + return; + } + } + if (!output) { + settle(() => + resolve({ + content: [{ type: "text", text: "No files found matching pattern" }], + details: undefined, + }), + ); + return; + } + + const relativized: string[] = []; + for (const rawLine of lines) { + const line = rawLine.replace(/\r$/, "").trim(); + if (!line) { + continue; + } + const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\"); + let relativePath = line; + if (line.startsWith(searchPath)) { + relativePath = line.slice(searchPath.length + 1); + } else { + relativePath = path.relative(searchPath, line); + } + if (hadTrailingSlash && !relativePath.endsWith("/")) { + relativePath += "/"; + } + relativized.push(toPosixPath(relativePath)); + } + + const resultLimitReached = relativized.length >= effectiveLimit; + const rawOutput = relativized.join("\n"); + const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); + let resultOutput = truncation.content; + const details: FindToolDetails = {}; + const notices: string[] = []; + if (resultLimitReached) { + notices.push( + `${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`, + ); + details.resultLimitReached = effectiveLimit; + } + if (truncation.truncated) { + notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); + details.truncation = truncation; + } + if (notices.length > 0) { + resultOutput += `\n\n[${notices.join(". ")}]`; + } + settle(() => + resolve({ + content: [{ type: "text", text: resultOutput }], + details: Object.keys(details).length > 0 ? details : undefined, + }), + ); + }); + } catch (e) { + if (signal?.aborted) { + settle(() => reject(new Error("Operation aborted"))); + return; + } + const error = e instanceof Error ? e : new Error(String(e)); + settle(() => reject(error)); + } + })(); + }); + }, + renderCall(args, theme, context) { + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + text.setText(formatFindCall(args, theme)); + return text; + }, + renderResult(result, options, theme, context) { + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + text.setText(formatFindResult(result, options, theme, context.showImages)); + return text; + }, + }; +} + +export function createFindTool( + cwd: string, + options?: FindToolOptions, +): AgentTool { + return wrapToolDefinition(createFindToolDefinition(cwd, options)); +} diff --git a/src/agents/sessions/tools/grep.ts b/src/agents/sessions/tools/grep.ts new file mode 100644 index 00000000000..68c37b1d135 --- /dev/null +++ b/src/agents/sessions/tools/grep.ts @@ -0,0 +1,445 @@ +import { spawn } from "node:child_process"; +import { readFileSync, statSync } from "node:fs"; +import path from "node:path"; +import { createInterface } from "node:readline"; +import { Text } from "@earendil-works/pi-tui"; +import { type Static, Type } from "typebox"; +import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; +import type { AgentTool } from "../../runtime/index.js"; +import { ensureTool } from "../../utils/tools-manager.js"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; +import { resolveToCwd } from "./path-utils.js"; +import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js"; +import { wrapToolDefinition } from "./tool-definition-wrapper.js"; +import { + DEFAULT_MAX_BYTES, + formatSize, + GREP_MAX_LINE_LENGTH, + type TruncationResult, + truncateHead, + truncateLine, +} from "./truncate.js"; + +const grepSchema = Type.Object({ + pattern: Type.String({ description: "Search pattern (regex or literal string)" }), + path: Type.Optional( + Type.String({ description: "Directory or file to search (default: current directory)" }), + ), + glob: Type.Optional( + Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" }), + ), + ignoreCase: Type.Optional( + Type.Boolean({ description: "Case-insensitive search (default: false)" }), + ), + literal: Type.Optional( + Type.Boolean({ + description: "Treat pattern as literal string instead of regex (default: false)", + }), + ), + context: Type.Optional( + Type.Number({ + description: "Number of lines to show before and after each match (default: 0)", + }), + ), + limit: Type.Optional( + Type.Number({ description: "Maximum number of matches to return (default: 100)" }), + ), +}); + +export type GrepToolInput = Static; +const DEFAULT_LIMIT = 100; + +export interface GrepToolDetails { + truncation?: TruncationResult; + matchLimitReached?: number; + linesTruncated?: boolean; +} + +/** + * Pluggable operations for the grep tool. + * Override these to delegate search to remote systems (for example SSH). + */ +export interface GrepOperations { + /** Check if path is a directory. Throws if path does not exist. */ + isDirectory: (absolutePath: string) => Promise | boolean; + /** Read file contents for context lines */ + readFile: (absolutePath: string) => Promise | string; +} + +const defaultGrepOperations: GrepOperations = { + isDirectory: (p) => statSync(p).isDirectory(), + readFile: (p) => readFileSync(p, "utf-8"), +}; + +export interface GrepToolOptions { + /** Custom operations for grep. Default: local filesystem plus ripgrep */ + operations?: GrepOperations; +} + +function formatGrepCall( + args: { pattern: string; path?: string; glob?: string; limit?: number } | undefined, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, +): string { + const pattern = str(args?.pattern); + const rawPath = str(args?.path); + const path = rawPath !== null ? shortenPath(rawPath || ".") : null; + const glob = str(args?.glob); + const limit = args?.limit; + const invalidArg = invalidArgText(theme); + let text = + theme.fg("toolTitle", theme.bold("grep")) + + " " + + (pattern === null ? invalidArg : theme.fg("accent", `/${pattern || ""}/`)) + + theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`); + if (glob) { + text += theme.fg("toolOutput", ` (${glob})`); + } + if (limit !== undefined) { + text += theme.fg("toolOutput", ` limit ${limit}`); + } + return text; +} + +function formatGrepResult( + result: { + content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; + details?: GrepToolDetails; + }, + options: ToolRenderResultOptions, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, + showImages: boolean, +): string { + const output = getTextOutput(result, showImages).trim(); + let text = ""; + if (output) { + const lines = output.split("\n"); + const maxLines = options.expanded ? lines.length : 15; + const displayLines = lines.slice(0, maxLines); + const remaining = lines.length - maxLines; + text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`; + if (remaining > 0) { + text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`; + } + } + + const matchLimit = result.details?.matchLimitReached; + const truncation = result.details?.truncation; + const linesTruncated = result.details?.linesTruncated; + if (matchLimit || truncation?.truncated || linesTruncated) { + const warnings: string[] = []; + if (matchLimit) { + warnings.push(`${matchLimit} matches limit`); + } + if (truncation?.truncated) { + warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`); + } + if (linesTruncated) { + warnings.push("some lines truncated"); + } + text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`; + } + return text; +} + +export function createGrepToolDefinition( + cwd: string, + options?: GrepToolOptions, +): ToolDefinition { + const customOps = options?.operations; + return { + name: "grep", + label: "grep", + description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`, + promptSnippet: "Search file contents for patterns (respects .gitignore)", + parameters: grepSchema, + async execute( + toolCallId, + { + pattern, + path: searchDir, + glob, + ignoreCase, + literal, + context, + limit, + }: { + pattern: string; + path?: string; + glob?: string; + ignoreCase?: boolean; + literal?: boolean; + context?: number; + limit?: number; + }, + signal?: AbortSignal, + onUpdate?, + ctx?, + ) { + void toolCallId; + void onUpdate; + void ctx; + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(new Error("Operation aborted")); + return; + } + let settled = false; + const settle = (fn: () => void) => { + if (!settled) { + settled = true; + fn(); + } + }; + + void (async () => { + try { + const rgPath = await ensureTool("rg", true); + if (!rgPath) { + settle(() => + reject(new Error("ripgrep (rg) is not available and could not be downloaded")), + ); + return; + } + + const searchPath = resolveToCwd(searchDir || ".", cwd); + const ops = customOps ?? defaultGrepOperations; + let isDirectory: boolean; + try { + isDirectory = await ops.isDirectory(searchPath); + } catch { + settle(() => reject(new Error(`Path not found: ${searchPath}`))); + return; + } + + const contextValue = context && context > 0 ? context : 0; + const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT); + const formatPath = (filePath: string): string => { + if (isDirectory) { + const relative = path.relative(searchPath, filePath); + if (relative && !relative.startsWith("..")) { + return relative.replace(/\\/g, "/"); + } + } + return path.basename(filePath); + }; + + const fileCache = new Map(); + const getFileLines = async (filePath: string): Promise => { + let lines = fileCache.get(filePath); + if (!lines) { + try { + const content = await ops.readFile(filePath); + lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n"); + } catch { + lines = []; + } + fileCache.set(filePath, lines); + } + return lines; + }; + + const args: string[] = ["--json", "--line-number", "--color=never", "--hidden"]; + if (ignoreCase) { + args.push("--ignore-case"); + } + if (literal) { + args.push("--fixed-strings"); + } + if (glob) { + args.push("--glob", glob); + } + args.push("--", pattern, searchPath); + + const child = spawn(rgPath, args, { stdio: ["ignore", "pipe", "pipe"] }); + const rl = createInterface({ input: child.stdout }); + let stderr = ""; + let matchCount = 0; + let matchLimitReached = false; + let linesTruncated = false; + let aborted = false; + let killedDueToLimit = false; + const outputLines: string[] = []; + + const cleanup = () => { + rl.close(); + signal?.removeEventListener("abort", onAbort); + }; + const stopChild = (dueToLimit = false) => { + if (!child.killed) { + killedDueToLimit = dueToLimit; + child.kill(); + } + }; + const onAbort = () => { + aborted = true; + stopChild(); + }; + signal?.addEventListener("abort", onAbort, { once: true }); + child.stderr?.on("data", (chunk) => { + stderr += chunk.toString(); + }); + + const formatBlock = async (filePath: string, lineNumber: number): Promise => { + const relativePath = formatPath(filePath); + const lines = await getFileLines(filePath); + if (!lines.length) { + return [`${relativePath}:${lineNumber}: (unable to read file)`]; + } + const block: string[] = []; + const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber; + const end = + contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber; + for (let current = start; current <= end; current++) { + const lineText = lines[current - 1] ?? ""; + const sanitized = lineText.replace(/\r/g, ""); + const isMatchLine = current === lineNumber; + // Truncate long lines so grep output stays compact. + const { text: truncatedText, wasTruncated } = truncateLine(sanitized); + if (wasTruncated) { + linesTruncated = true; + } + if (isMatchLine) { + block.push(`${relativePath}:${current}: ${truncatedText}`); + } else { + block.push(`${relativePath}-${current}- ${truncatedText}`); + } + } + return block; + }; + + // Collect matches during streaming, then format them after rg exits. + const matches: Array<{ filePath: string; lineNumber: number; lineText?: string }> = []; + rl.on("line", (line) => { + if (!line.trim() || matchCount >= effectiveLimit) { + return; + } + let event: { + type?: string; + data?: { + path?: { text?: string }; + line_number?: unknown; + lines?: { text?: string }; + }; + }; + try { + event = JSON.parse(line); + } catch { + return; + } + if (event.type === "match") { + matchCount++; + const filePath = event.data?.path?.text; + const lineNumber = event.data?.line_number; + const lineText = event.data?.lines?.text; + if (filePath && typeof lineNumber === "number") { + matches.push({ filePath, lineNumber, lineText }); + } + if (matchCount >= effectiveLimit) { + matchLimitReached = true; + stopChild(true); + } + } + }); + + child.on("error", (error) => { + cleanup(); + settle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`))); + }); + child.on("close", async (code) => { + cleanup(); + if (aborted) { + settle(() => reject(new Error("Operation aborted"))); + return; + } + if (!killedDueToLimit && code !== 0 && code !== 1) { + const errorMsg = stderr.trim() || `ripgrep exited with code ${code}`; + settle(() => reject(new Error(errorMsg))); + return; + } + if (matchCount === 0) { + settle(() => + resolve({ + content: [{ type: "text", text: "No matches found" }], + details: undefined, + }), + ); + return; + } + + // Format matches after streaming finishes so custom readFile() backends can be async. + for (const match of matches) { + if (contextValue === 0 && match.lineText !== undefined) { + const relativePath = formatPath(match.filePath); + const sanitized = match.lineText + .replace(/\r\n/g, "\n") + .replace(/\r/g, "") + .replace(/\n$/, ""); + const { text: truncatedText, wasTruncated } = truncateLine(sanitized); + if (wasTruncated) { + linesTruncated = true; + } + outputLines.push(`${relativePath}:${match.lineNumber}: ${truncatedText}`); + } else { + const block = await formatBlock(match.filePath, match.lineNumber); + outputLines.push(...block); + } + } + + const rawOutput = outputLines.join("\n"); + // Apply byte truncation. There is no line limit here because the match limit already capped rows. + const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); + let output = truncation.content; + const details: GrepToolDetails = {}; + // Build actionable notices for truncation and match limits. + const notices: string[] = []; + if (matchLimitReached) { + notices.push( + `${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`, + ); + details.matchLimitReached = effectiveLimit; + } + if (truncation.truncated) { + notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); + details.truncation = truncation; + } + if (linesTruncated) { + notices.push( + `Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`, + ); + details.linesTruncated = true; + } + if (notices.length > 0) { + output += `\n\n[${notices.join(". ")}]`; + } + settle(() => + resolve({ + content: [{ type: "text", text: output }], + details: Object.keys(details).length > 0 ? details : undefined, + }), + ); + }); + } catch (err) { + settle(() => reject(err as Error)); + } + })(); + }); + }, + renderCall(args, theme, context) { + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + text.setText(formatGrepCall(args, theme)); + return text; + }, + renderResult(result, options, theme, context) { + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + text.setText(formatGrepResult(result, options, theme, context.showImages)); + return text; + }, + }; +} + +export function createGrepTool( + cwd: string, + options?: GrepToolOptions, +): AgentTool { + return wrapToolDefinition(createGrepToolDefinition(cwd, options)); +} diff --git a/src/agents/sessions/tools/index.ts b/src/agents/sessions/tools/index.ts new file mode 100644 index 00000000000..7eed1afbf5b --- /dev/null +++ b/src/agents/sessions/tools/index.ts @@ -0,0 +1,211 @@ +export { + type BashOperations, + type BashSpawnContext, + type BashSpawnHook, + type BashToolDetails, + type BashToolInput, + type BashToolOptions, + createBashTool, + createBashToolDefinition, + createLocalBashOperations, +} from "./bash.js"; +export { + createEditTool, + createEditToolDefinition, + type EditOperations, + type EditToolDetails, + type EditToolInput, + type EditToolOptions, +} from "./edit.js"; +export { withFileMutationQueue } from "./file-mutation-queue.js"; +export { + createFindTool, + createFindToolDefinition, + type FindOperations, + type FindToolDetails, + type FindToolInput, + type FindToolOptions, +} from "./find.js"; +export { + createGrepTool, + createGrepToolDefinition, + type GrepOperations, + type GrepToolDetails, + type GrepToolInput, + type GrepToolOptions, +} from "./grep.js"; +export { + createLsTool, + createLsToolDefinition, + type LsOperations, + type LsToolDetails, + type LsToolInput, + type LsToolOptions, +} from "./ls.js"; +export { + createReadTool, + createReadToolDefinition, + type ReadOperations, + type ReadToolDetails, + type ReadToolInput, + type ReadToolOptions, +} from "./read.js"; +export { + DEFAULT_MAX_BYTES, + DEFAULT_MAX_LINES, + formatSize, + type TruncationOptions, + type TruncationResult, + truncateHead, + truncateLine, + truncateTail, +} from "./truncate.js"; +export { + createWriteTool, + createWriteToolDefinition, + type WriteOperations, + type WriteToolInput, + type WriteToolOptions, +} from "./write.js"; + +import type { AgentTool } from "../../runtime/index.js"; +import type { ToolDefinition } from "../extensions/types.js"; +import { type BashToolOptions, createBashTool, createBashToolDefinition } from "./bash.js"; +import { createEditTool, createEditToolDefinition, type EditToolOptions } from "./edit.js"; +import { createFindTool, createFindToolDefinition, type FindToolOptions } from "./find.js"; +import { createGrepTool, createGrepToolDefinition, type GrepToolOptions } from "./grep.js"; +import { createLsTool, createLsToolDefinition, type LsToolOptions } from "./ls.js"; +import { createReadTool, createReadToolDefinition, type ReadToolOptions } from "./read.js"; +import { createWriteTool, createWriteToolDefinition, type WriteToolOptions } from "./write.js"; + +export type Tool = AgentTool; +export type ToolDef = ToolDefinition; +export type ToolName = "read" | "bash" | "edit" | "write" | "grep" | "find" | "ls"; +export const allToolNames: Set = new Set([ + "read", + "bash", + "edit", + "write", + "grep", + "find", + "ls", +]); + +export interface ToolsOptions { + read?: ReadToolOptions; + bash?: BashToolOptions; + write?: WriteToolOptions; + edit?: EditToolOptions; + grep?: GrepToolOptions; + find?: FindToolOptions; + ls?: LsToolOptions; +} + +export function createToolDefinition( + toolName: ToolName, + cwd: string, + options?: ToolsOptions, +): ToolDef { + switch (toolName) { + case "read": + return createReadToolDefinition(cwd, options?.read); + case "bash": + return createBashToolDefinition(cwd, options?.bash); + case "edit": + return createEditToolDefinition(cwd, options?.edit); + case "write": + return createWriteToolDefinition(cwd, options?.write); + case "grep": + return createGrepToolDefinition(cwd, options?.grep); + case "find": + return createFindToolDefinition(cwd, options?.find); + case "ls": + return createLsToolDefinition(cwd, options?.ls); + default: + throw new Error(`Unknown tool name: ${String(toolName)}`); + } +} + +export function createTool(toolName: ToolName, cwd: string, options?: ToolsOptions): Tool { + switch (toolName) { + case "read": + return createReadTool(cwd, options?.read); + case "bash": + return createBashTool(cwd, options?.bash); + case "edit": + return createEditTool(cwd, options?.edit); + case "write": + return createWriteTool(cwd, options?.write); + case "grep": + return createGrepTool(cwd, options?.grep); + case "find": + return createFindTool(cwd, options?.find); + case "ls": + return createLsTool(cwd, options?.ls); + default: + throw new Error(`Unknown tool name: ${String(toolName)}`); + } +} + +export function createCodingToolDefinitions(cwd: string, options?: ToolsOptions): ToolDef[] { + return [ + createReadToolDefinition(cwd, options?.read), + createBashToolDefinition(cwd, options?.bash), + createEditToolDefinition(cwd, options?.edit), + createWriteToolDefinition(cwd, options?.write), + ]; +} + +export function createReadOnlyToolDefinitions(cwd: string, options?: ToolsOptions): ToolDef[] { + return [ + createReadToolDefinition(cwd, options?.read), + createGrepToolDefinition(cwd, options?.grep), + createFindToolDefinition(cwd, options?.find), + createLsToolDefinition(cwd, options?.ls), + ]; +} + +export function createAllToolDefinitions( + cwd: string, + options?: ToolsOptions, +): Record { + return { + read: createReadToolDefinition(cwd, options?.read), + bash: createBashToolDefinition(cwd, options?.bash), + edit: createEditToolDefinition(cwd, options?.edit), + write: createWriteToolDefinition(cwd, options?.write), + grep: createGrepToolDefinition(cwd, options?.grep), + find: createFindToolDefinition(cwd, options?.find), + ls: createLsToolDefinition(cwd, options?.ls), + }; +} + +export function createCodingTools(cwd: string, options?: ToolsOptions): Tool[] { + return [ + createReadTool(cwd, options?.read), + createBashTool(cwd, options?.bash), + createEditTool(cwd, options?.edit), + createWriteTool(cwd, options?.write), + ]; +} + +export function createReadOnlyTools(cwd: string, options?: ToolsOptions): Tool[] { + return [ + createReadTool(cwd, options?.read), + createGrepTool(cwd, options?.grep), + createFindTool(cwd, options?.find), + createLsTool(cwd, options?.ls), + ]; +} + +export function createAllTools(cwd: string, options?: ToolsOptions): Record { + return { + read: createReadTool(cwd, options?.read), + bash: createBashTool(cwd, options?.bash), + edit: createEditTool(cwd, options?.edit), + write: createWriteTool(cwd, options?.write), + grep: createGrepTool(cwd, options?.grep), + find: createFindTool(cwd, options?.find), + ls: createLsTool(cwd, options?.ls), + }; +} diff --git a/src/agents/sessions/tools/ls.ts b/src/agents/sessions/tools/ls.ts new file mode 100644 index 00000000000..6ee5c1fa640 --- /dev/null +++ b/src/agents/sessions/tools/ls.ts @@ -0,0 +1,250 @@ +import { existsSync, readdirSync, statSync } from "node:fs"; +import nodePath from "node:path"; +import { Text } from "@earendil-works/pi-tui"; +import { type Static, Type } from "typebox"; +import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; +import type { AgentTool } from "../../runtime/index.js"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; +import { resolveToCwd } from "./path-utils.js"; +import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js"; +import { wrapToolDefinition } from "./tool-definition-wrapper.js"; +import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.js"; + +const lsSchema = Type.Object({ + path: Type.Optional( + Type.String({ description: "Directory to list (default: current directory)" }), + ), + limit: Type.Optional( + Type.Number({ description: "Maximum number of entries to return (default: 500)" }), + ), +}); + +export type LsToolInput = Static; + +const DEFAULT_LIMIT = 500; + +export interface LsToolDetails { + truncation?: TruncationResult; + entryLimitReached?: number; +} + +/** + * Pluggable operations for the ls tool. + * Override these to delegate directory listing to remote systems (for example SSH). + */ +export interface LsOperations { + /** Check if path exists */ + exists: (absolutePath: string) => Promise | boolean; + /** Get file or directory stats. Throws if not found. */ + stat: ( + absolutePath: string, + ) => Promise<{ isDirectory: () => boolean }> | { isDirectory: () => boolean }; + /** Read directory entries */ + readdir: (absolutePath: string) => Promise | string[]; +} + +const defaultLsOperations: LsOperations = { + exists: existsSync, + stat: statSync, + readdir: readdirSync, +}; + +export interface LsToolOptions { + /** Custom operations for directory listing. Default: local filesystem */ + operations?: LsOperations; +} + +function formatLsCall( + args: { path?: string; limit?: number } | undefined, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, +): string { + const rawPath = str(args?.path); + const path = rawPath !== null ? shortenPath(rawPath || ".") : null; + const limit = args?.limit; + const invalidArg = invalidArgText(theme); + let text = `${theme.fg("toolTitle", theme.bold("ls"))} ${path === null ? invalidArg : theme.fg("accent", path)}`; + if (limit !== undefined) { + text += theme.fg("toolOutput", ` (limit ${limit})`); + } + return text; +} + +function formatLsResult( + result: { + content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; + details?: LsToolDetails; + }, + options: ToolRenderResultOptions, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, + showImages: boolean, +): string { + const output = getTextOutput(result, showImages).trim(); + let text = ""; + if (output) { + const lines = output.split("\n"); + const maxLines = options.expanded ? lines.length : 20; + const displayLines = lines.slice(0, maxLines); + const remaining = lines.length - maxLines; + text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`; + if (remaining > 0) { + text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`; + } + } + + const entryLimit = result.details?.entryLimitReached; + const truncation = result.details?.truncation; + if (entryLimit || truncation?.truncated) { + const warnings: string[] = []; + if (entryLimit) { + warnings.push(`${entryLimit} entries limit`); + } + if (truncation?.truncated) { + warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`); + } + text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`; + } + return text; +} + +export function createLsToolDefinition( + cwd: string, + options?: LsToolOptions, +): ToolDefinition { + const ops = options?.operations ?? defaultLsOperations; + return { + name: "ls", + label: "ls", + description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`, + promptSnippet: "List directory contents", + parameters: lsSchema, + async execute( + toolCallId, + { path, limit }: { path?: string; limit?: number }, + signal?: AbortSignal, + onUpdate?, + ctx?, + ) { + void toolCallId; + void onUpdate; + void ctx; + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(new Error("Operation aborted")); + return; + } + + const onAbort = () => reject(new Error("Operation aborted")); + signal?.addEventListener("abort", onAbort, { once: true }); + + void (async () => { + try { + const dirPath = resolveToCwd(path || ".", cwd); + const effectiveLimit = limit ?? DEFAULT_LIMIT; + + // Check if path exists. + if (!(await ops.exists(dirPath))) { + reject(new Error(`Path not found: ${dirPath}`)); + return; + } + + // Check if path is a directory. + const stat = await ops.stat(dirPath); + if (!stat.isDirectory()) { + reject(new Error(`Not a directory: ${dirPath}`)); + return; + } + + // Read directory entries. + let entries: string[]; + try { + entries = await ops.readdir(dirPath); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + reject(new Error(`Cannot read directory: ${message}`)); + return; + } + + // Sort alphabetically, case-insensitive. + entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); + + // Format entries with directory indicators. + const results: string[] = []; + let entryLimitReached = false; + for (const entry of entries) { + if (results.length >= effectiveLimit) { + entryLimitReached = true; + break; + } + + const fullPath = nodePath.join(dirPath, entry); + let suffix = ""; + try { + const entryStat = await ops.stat(fullPath); + if (entryStat.isDirectory()) { + suffix = "/"; + } + } catch { + // Skip entries we cannot stat. + continue; + } + results.push(entry + suffix); + } + + signal?.removeEventListener("abort", onAbort); + + if (results.length === 0) { + resolve({ + content: [{ type: "text", text: "(empty directory)" }], + details: undefined, + }); + return; + } + + const rawOutput = results.join("\n"); + // Apply byte truncation. There is no separate line limit because entry count is already capped. + const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); + let output = truncation.content; + const details: LsToolDetails = {}; + // Build actionable notices for truncation and entry limits. + const notices: string[] = []; + if (entryLimitReached) { + notices.push( + `${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`, + ); + details.entryLimitReached = effectiveLimit; + } + if (truncation.truncated) { + notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); + details.truncation = truncation; + } + if (notices.length > 0) { + output += `\n\n[${notices.join(". ")}]`; + } + + resolve({ + content: [{ type: "text", text: output }], + details: Object.keys(details).length > 0 ? details : undefined, + }); + } catch (e: unknown) { + signal?.removeEventListener("abort", onAbort); + reject(e); + } + })(); + }); + }, + renderCall(args, theme, context) { + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + text.setText(formatLsCall(args, theme)); + return text; + }, + renderResult(result, options, theme, context) { + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + text.setText(formatLsResult(result, options, theme, context.showImages)); + return text; + }, + }; +} + +export function createLsTool(cwd: string, options?: LsToolOptions): AgentTool { + return wrapToolDefinition(createLsToolDefinition(cwd, options)); +} diff --git a/src/agents/sessions/tools/output-accumulator.ts b/src/agents/sessions/tools/output-accumulator.ts new file mode 100644 index 00000000000..bb72d757d2d --- /dev/null +++ b/src/agents/sessions/tools/output-accumulator.ts @@ -0,0 +1,230 @@ +import { randomBytes } from "node:crypto"; +import { createWriteStream, type WriteStream } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { + DEFAULT_MAX_BYTES, + DEFAULT_MAX_LINES, + type TruncationResult, + truncateTail, +} from "./truncate.js"; + +export interface OutputAccumulatorOptions { + maxLines?: number; + maxBytes?: number; + tempFilePrefix?: string; +} + +export interface OutputSnapshot { + content: string; + truncation: TruncationResult; + fullOutputPath?: string; +} + +function defaultTempFilePath(prefix: string): string { + const id = randomBytes(8).toString("hex"); + return join(tmpdir(), `${prefix}-${id}.log`); +} + +function byteLength(text: string): number { + return Buffer.byteLength(text, "utf-8"); +} + +/** + * Incrementally tracks streaming output with bounded memory. + * + * Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded + * tail for display snapshots, and opens a temp file when the full output needs + * to be preserved. + */ +export class OutputAccumulator { + private readonly maxLines: number; + private readonly maxBytes: number; + private readonly maxRollingBytes: number; + private readonly tempFilePrefix: string; + private readonly decoder = new TextDecoder(); + + private rawChunks: Buffer[] = []; + private tailText = ""; + private tailBytes = 0; + private tailStartsAtLineBoundary = true; + private totalRawBytes = 0; + private totalDecodedBytes = 0; + private completedLines = 0; + private totalLines = 0; + private currentLineBytes = 0; + private hasOpenLine = false; + private finished = false; + + private tempFilePath: string | undefined; + private tempFileStream: WriteStream | undefined; + + constructor(options: OutputAccumulatorOptions = {}) { + this.maxLines = options.maxLines ?? DEFAULT_MAX_LINES; + this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; + this.maxRollingBytes = Math.max(this.maxBytes * 2, 1); + this.tempFilePrefix = options.tempFilePrefix ?? "openclaw-output"; + } + + append(data: Buffer): void { + if (this.finished) { + throw new Error("Cannot append to a finished output accumulator"); + } + + this.totalRawBytes += data.length; + this.appendDecodedText(this.decoder.decode(data, { stream: true })); + + if (this.tempFileStream || this.shouldUseTempFile()) { + this.ensureTempFile(); + this.tempFileStream?.write(data); + } else if (data.length > 0) { + this.rawChunks.push(data); + } + } + + finish(): void { + if (this.finished) { + return; + } + this.finished = true; + this.appendDecodedText(this.decoder.decode()); + if (this.shouldUseTempFile()) { + this.ensureTempFile(); + } + } + + snapshot(options: { persistIfTruncated?: boolean } = {}): OutputSnapshot { + const tailTruncation = truncateTail(this.getSnapshotText(), { + maxLines: this.maxLines, + maxBytes: this.maxBytes, + }); + const truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes; + const truncatedBy = truncated + ? (tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? "bytes" : "lines")) + : null; + const truncation: TruncationResult = { + ...tailTruncation, + truncated, + truncatedBy, + totalLines: this.totalLines, + totalBytes: this.totalDecodedBytes, + maxLines: this.maxLines, + maxBytes: this.maxBytes, + }; + + if (options.persistIfTruncated && truncation.truncated) { + this.ensureTempFile(); + } + + return { + content: truncation.content, + truncation, + fullOutputPath: this.tempFilePath, + }; + } + + async closeTempFile(): Promise { + if (!this.tempFileStream) { + return; + } + + const stream = this.tempFileStream; + this.tempFileStream = undefined; + + await new Promise((resolve, reject) => { + const onError = (error: Error) => { + stream.off("finish", onFinish); + reject(error); + }; + const onFinish = () => { + stream.off("error", onError); + resolve(); + }; + stream.once("error", onError); + stream.once("finish", onFinish); + stream.end(); + }); + } + + getLastLineBytes(): number { + return this.currentLineBytes; + } + + private appendDecodedText(text: string): void { + if (text.length === 0) { + return; + } + + const bytes = byteLength(text); + this.totalDecodedBytes += bytes; + this.tailText += text; + this.tailBytes += bytes; + if (this.tailBytes > this.maxRollingBytes * 2) { + this.trimTail(); + } + + let newlines = 0; + let lastNewline = -1; + for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) { + newlines++; + lastNewline = i; + } + if (newlines === 0) { + this.currentLineBytes += bytes; + this.hasOpenLine = true; + } else { + this.completedLines += newlines; + const tail = text.slice(lastNewline + 1); + this.currentLineBytes = byteLength(tail); + this.hasOpenLine = tail.length > 0; + } + this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0); + } + + private trimTail(): void { + const buffer = Buffer.from(this.tailText, "utf-8"); + if (buffer.length <= this.maxRollingBytes) { + this.tailBytes = buffer.length; + return; + } + + let start = buffer.length - this.maxRollingBytes; + while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) { + start++; + } + + this.tailStartsAtLineBoundary = + start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 0x0a; + this.tailText = buffer.subarray(start).toString("utf-8"); + this.tailBytes = byteLength(this.tailText); + } + + private getSnapshotText(): string { + if (this.tailStartsAtLineBoundary) { + return this.tailText; + } + + const firstNewline = this.tailText.indexOf("\n"); + return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1); + } + + private shouldUseTempFile(): boolean { + return ( + this.totalRawBytes > this.maxBytes || + this.totalDecodedBytes > this.maxBytes || + this.totalLines > this.maxLines + ); + } + + private ensureTempFile(): void { + if (this.tempFilePath) { + return; + } + this.tempFilePath = defaultTempFilePath(this.tempFilePrefix); + this.tempFileStream = createWriteStream(this.tempFilePath); + for (const chunk of this.rawChunks) { + this.tempFileStream.write(chunk); + } + this.rawChunks = []; + } +} diff --git a/src/agents/sessions/tools/path-utils.ts b/src/agents/sessions/tools/path-utils.ts new file mode 100644 index 00000000000..62b6148592b --- /dev/null +++ b/src/agents/sessions/tools/path-utils.ts @@ -0,0 +1,94 @@ +import { accessSync, constants } from "node:fs"; +import * as os from "node:os"; +import { isAbsolute, resolve as resolvePath } from "node:path"; + +const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g; +const NARROW_NO_BREAK_SPACE = "\u202F"; +function normalizeUnicodeSpaces(str: string): string { + return str.replace(UNICODE_SPACES, " "); +} + +function tryMacOSScreenshotPath(filePath: string): string { + return filePath.replace(/ (AM|PM)\./gi, `${NARROW_NO_BREAK_SPACE}$1.`); +} + +function tryNFDVariant(filePath: string): string { + // macOS stores filenames in NFD (decomposed) form, try converting user input to NFD + return filePath.normalize("NFD"); +} + +function tryCurlyQuoteVariant(filePath: string): string { + // macOS uses U+2019 (right single quotation mark) in screenshot names like "Capture d'écran" + // Users typically type U+0027 (straight apostrophe) + return filePath.replace(/'/g, "\u2019"); +} + +function fileExists(filePath: string): boolean { + try { + accessSync(filePath, constants.F_OK); + return true; + } catch { + return false; + } +} + +function normalizeAtPrefix(filePath: string): string { + return filePath.startsWith("@") ? filePath.slice(1) : filePath; +} + +export function expandPath(filePath: string): string { + const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath)); + if (normalized === "~") { + return os.homedir(); + } + if (normalized.startsWith("~/")) { + return os.homedir() + normalized.slice(1); + } + return normalized; +} + +/** + * Resolve a path relative to the given cwd. + * Handles ~ expansion and absolute paths. + */ +export function resolveToCwd(filePath: string, cwd: string): string { + const expanded = expandPath(filePath); + if (isAbsolute(expanded)) { + return expanded; + } + return resolvePath(cwd, expanded); +} + +export function resolveReadPath(filePath: string, cwd: string): string { + const resolved = resolveToCwd(filePath, cwd); + + if (fileExists(resolved)) { + return resolved; + } + + // Try macOS AM/PM variant (narrow no-break space before AM/PM) + const amPmVariant = tryMacOSScreenshotPath(resolved); + if (amPmVariant !== resolved && fileExists(amPmVariant)) { + return amPmVariant; + } + + // Try NFD variant (macOS stores filenames in NFD form) + const nfdVariant = tryNFDVariant(resolved); + if (nfdVariant !== resolved && fileExists(nfdVariant)) { + return nfdVariant; + } + + // Try curly quote variant (macOS uses U+2019 in screenshot names) + const curlyVariant = tryCurlyQuoteVariant(resolved); + if (curlyVariant !== resolved && fileExists(curlyVariant)) { + return curlyVariant; + } + + // Try combined NFD + curly quote (for French macOS screenshots like "Capture d'écran") + const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant); + if (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) { + return nfdCurlyVariant; + } + + return resolved; +} diff --git a/src/agents/sessions/tools/read.ts b/src/agents/sessions/tools/read.ts new file mode 100644 index 00000000000..b6c46a3801d --- /dev/null +++ b/src/agents/sessions/tools/read.ts @@ -0,0 +1,423 @@ +import { constants } from "node:fs"; +import { access as fsAccess, readFile as fsReadFile } from "node:fs/promises"; +import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from "node:path"; +import { Text } from "@earendil-works/pi-tui"; +import type { ImageContent, Model, TextContent } from "openclaw/plugin-sdk/llm"; +import { type Static, Type } from "typebox"; +import { getReadmePath } from "../../config.js"; +import { keyHint, keyText } from "../../modes/interactive/components/keybinding-hints.js"; +import { + getLanguageFromPath, + highlightCode, + type Theme, +} from "../../modes/interactive/theme/theme.js"; +import type { AgentTool } from "../../runtime/index.js"; +import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js"; +import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js"; +import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; +import { resolveReadPath } from "./path-utils.js"; +import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.js"; +import { wrapToolDefinition } from "./tool-definition-wrapper.js"; +import { + DEFAULT_MAX_BYTES, + DEFAULT_MAX_LINES, + formatSize, + type TruncationResult, + truncateHead, +} from "./truncate.js"; + +const readSchema = Type.Object({ + path: Type.String({ description: "Path to the file to read (relative or absolute)" }), + offset: Type.Optional( + Type.Number({ description: "Line number to start reading from (1-indexed)" }), + ), + limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })), +}); + +export type ReadToolInput = Static; + +export interface ReadToolDetails { + truncation?: TruncationResult; +} + +interface CompactReadClassification { + kind: "docs" | "resource" | "skill"; + label: string; +} + +const COMPACT_RESOURCE_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]); + +/** + * Pluggable operations for the read tool. + * Override these to delegate file reading to remote systems (for example SSH). + */ +export interface ReadOperations { + /** Read file contents as a Buffer */ + readFile: (absolutePath: string) => Promise; + /** Check if file is readable (throw if not) */ + access: (absolutePath: string) => Promise; + /** Detect image MIME type, return null or undefined for non-images */ + detectImageMimeType?: (absolutePath: string) => Promise; +} + +const defaultReadOperations: ReadOperations = { + readFile: (path) => fsReadFile(path), + access: (path) => fsAccess(path, constants.R_OK), + detectImageMimeType: detectSupportedImageMimeTypeFromFile, +}; + +export interface ReadToolOptions { + /** Whether to auto-resize images to 2000x2000 max. Default: true */ + autoResizeImages?: boolean; + /** Custom operations for file reading. Default: local filesystem */ + operations?: ReadOperations; +} + +type ReadRenderArgs = { path?: string; file_path?: string; offset?: number; limit?: number }; + +function formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): string { + if (args?.offset === undefined && args?.limit === undefined) { + return ""; + } + const startLine = args.offset ?? 1; + const endLine = args.limit !== undefined ? startLine + args.limit - 1 : ""; + return theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`); +} + +function formatReadCall(args: ReadRenderArgs | undefined, theme: Theme): string { + const rawPath = str(args?.file_path ?? args?.path); + const path = rawPath !== null ? shortenPath(rawPath) : null; + const invalidArg = invalidArgText(theme); + const pathDisplay = + path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."); + return `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}${formatReadLineRange(args, theme)}`; +} + +function trimTrailingEmptyLines(lines: string[]): string[] { + let end = lines.length; + while (end > 0 && lines[end - 1] === "") { + end--; + } + return lines.slice(0, end); +} + +function getNonVisionImageNote(model: Model | undefined): string | undefined { + if (!model || model.input.includes("image")) { + return undefined; + } + return "[Current model does not support images. The image will be omitted from this request.]"; +} + +function toPosixPath(filePath: string): string { + return filePath.split(sep).join("/"); +} + +function getOpenClawDocsClassification( + absolutePath: string, +): CompactReadClassification | undefined { + const packageRoot = dirname(getReadmePath()); + const relativePath = relative(resolvePath(packageRoot), resolvePath(absolutePath)); + if ( + relativePath === "" || + relativePath === ".." || + relativePath.startsWith(`..${sep}`) || + isAbsolute(relativePath) + ) { + return undefined; + } + + const label = toPosixPath(relativePath); + if (label === "README.md" || label.startsWith("docs/") || label.startsWith("examples/")) { + return { kind: "docs", label }; + } + return undefined; +} + +function getCompactReadClassification( + args: ReadRenderArgs | undefined, + cwd: string, +): CompactReadClassification | undefined { + const rawPath = str(args?.file_path ?? args?.path); + if (!rawPath) { + return undefined; + } + + const absolutePath = resolveReadPath(rawPath, cwd); + const fileName = basename(absolutePath); + if (fileName === "SKILL.md") { + return { kind: "skill", label: basename(dirname(absolutePath)) || fileName }; + } + + const docsClassification = getOpenClawDocsClassification(absolutePath); + if (docsClassification) { + return docsClassification; + } + + if (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) { + return { kind: "resource", label: formatPathRelativeToCwdOrAbsolute(absolutePath, cwd) }; + } + + return undefined; +} + +function formatCompactReadCall( + classification: CompactReadClassification, + args: ReadRenderArgs | undefined, + theme: Theme, +): string { + const expandHint = theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`); + if (classification.kind === "skill") { + return ( + theme.fg("customMessageLabel", `\u001b[1m[skill]\u001b[22m `) + + theme.fg("customMessageText", classification.label) + + formatReadLineRange(args, theme) + + expandHint + ); + } + + return ( + theme.fg("toolTitle", theme.bold(`read ${classification.kind}`)) + + " " + + theme.fg("accent", classification.label) + + formatReadLineRange(args, theme) + + expandHint + ); +} + +function formatReadResult( + args: ReadRenderArgs | undefined, + result: { content: (TextContent | ImageContent)[]; details?: ReadToolDetails }, + options: ToolRenderResultOptions, + theme: Theme, + showImages: boolean, + cwd: string, + isError: boolean, +): string { + if (!options.expanded && !isError && getCompactReadClassification(args, cwd)) { + return ""; + } + + const rawPath = str(args?.file_path ?? args?.path); + const output = getTextOutput(result, showImages); + const lang = rawPath ? getLanguageFromPath(rawPath) : undefined; + const renderedLines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n"); + const lines = trimTrailingEmptyLines(renderedLines); + const maxLines = options.expanded ? lines.length : 10; + const displayLines = lines.slice(0, maxLines); + const remaining = lines.length - maxLines; + let text = `\n${displayLines.map((line) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line)))).join("\n")}`; + if (remaining > 0) { + text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")})`; + } + + const truncation = result.details?.truncation; + if (truncation?.truncated) { + if (truncation.firstLineExceedsLimit) { + text += `\n${theme.fg("warning", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`)}`; + } else if (truncation.truncatedBy === "lines") { + text += `\n${theme.fg("warning", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`)}`; + } else { + text += `\n${theme.fg("warning", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`)}`; + } + } + return text; +} + +export function createReadToolDefinition( + cwd: string, + options?: ReadToolOptions, +): ToolDefinition { + const autoResizeImages = options?.autoResizeImages ?? true; + const ops = options?.operations ?? defaultReadOperations; + return { + name: "read", + label: "read", + description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`, + promptSnippet: "Read file contents", + promptGuidelines: ["Use read to examine files instead of cat or sed."], + parameters: readSchema, + async execute( + toolCallId, + { path, offset, limit }: { path: string; offset?: number; limit?: number }, + signal?: AbortSignal, + onUpdate?, + ctx?, + ) { + void toolCallId; + void onUpdate; + const absolutePath = resolveReadPath(path, cwd); + return new Promise<{ + content: (TextContent | ImageContent)[]; + details: ReadToolDetails | undefined; + }>((resolve, reject) => { + if (signal?.aborted) { + reject(new Error("Operation aborted")); + return; + } + let aborted = false; + const onAbort = () => { + aborted = true; + reject(new Error("Operation aborted")); + }; + signal?.addEventListener("abort", onAbort, { once: true }); + + void (async () => { + try { + // Check if file exists and is readable. + await ops.access(absolutePath); + if (aborted) { + return; + } + const mimeType = ops.detectImageMimeType + ? await ops.detectImageMimeType(absolutePath) + : undefined; + let content: (TextContent | ImageContent)[]; + let details: ReadToolDetails | undefined; + const nonVisionImageNote = getNonVisionImageNote(ctx?.model); + if (mimeType) { + // Read image as binary. + const buffer = await ops.readFile(absolutePath); + const base64 = buffer.toString("base64"); + if (autoResizeImages) { + // Resize image if needed before sending it back to the model. + const resized = await resizeImage({ type: "image", data: base64, mimeType }); + if (!resized) { + let textNote = `Read image file [${mimeType}]\n[Image omitted: could not be resized below the inline image size limit.]`; + if (nonVisionImageNote) { + textNote += `\n${nonVisionImageNote}`; + } + content = [{ type: "text", text: textNote }]; + } else { + const dimensionNote = formatDimensionNote(resized); + let textNote = `Read image file [${resized.mimeType}]`; + if (dimensionNote) { + textNote += `\n${dimensionNote}`; + } + if (nonVisionImageNote) { + textNote += `\n${nonVisionImageNote}`; + } + content = [ + { type: "text", text: textNote }, + { type: "image", data: resized.data, mimeType: resized.mimeType }, + ]; + } + } else { + let textNote = `Read image file [${mimeType}]`; + if (nonVisionImageNote) { + textNote += `\n${nonVisionImageNote}`; + } + content = [ + { type: "text", text: textNote }, + { type: "image", data: base64, mimeType }, + ]; + } + } else { + // Read text content. + const buffer = await ops.readFile(absolutePath); + const textContent = buffer.toString("utf-8"); + const allLines = textContent.split("\n"); + const totalFileLines = allLines.length; + // Apply offset if specified. Convert from 1-indexed input to 0-indexed array access. + const startLine = offset ? Math.max(0, offset - 1) : 0; + const startLineDisplay = startLine + 1; + // Check if offset is out of bounds. + if (startLine >= allLines.length) { + throw new Error( + `Offset ${offset} is beyond end of file (${allLines.length} lines total)`, + ); + } + let selectedContent: string; + let userLimitedLines: number | undefined; + // If limit is specified by the user, honor it first. Otherwise truncateHead decides. + if (limit !== undefined) { + const endLine = Math.min(startLine + limit, allLines.length); + selectedContent = allLines.slice(startLine, endLine).join("\n"); + userLimitedLines = endLine - startLine; + } else { + selectedContent = allLines.slice(startLine).join("\n"); + } + // Apply truncation, respecting both line and byte limits. + const truncation = truncateHead(selectedContent); + let outputText: string; + if (truncation.firstLineExceedsLimit) { + // First line alone exceeds the byte limit. Point the model at a bash fallback. + const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], "utf-8")); + outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`; + details = { truncation }; + } else if (truncation.truncated) { + // Truncation occurred. Build an actionable continuation notice. + const endLineDisplay = startLineDisplay + truncation.outputLines - 1; + const nextOffset = endLineDisplay + 1; + outputText = truncation.content; + if (truncation.truncatedBy === "lines") { + outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`; + } else { + outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`; + } + details = { truncation }; + } else if ( + userLimitedLines !== undefined && + startLine + userLimitedLines < allLines.length + ) { + // User-specified limit stopped early, but the file still has more content. + const remaining = allLines.length - (startLine + userLimitedLines); + const nextOffset = startLine + userLimitedLines + 1; + outputText = `${truncation.content}\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`; + } else { + // No truncation and no remaining user-limited content. + outputText = truncation.content; + } + content = [{ type: "text", text: outputText }]; + } + + if (aborted) { + return; + } + signal?.removeEventListener("abort", onAbort); + resolve({ content, details }); + } catch (error: unknown) { + signal?.removeEventListener("abort", onAbort); + if (!aborted) { + reject(error); + } + } + })(); + }); + }, + renderCall(args, theme, context) { + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + const classification = !context.expanded + ? getCompactReadClassification(args, context.cwd) + : undefined; + text.setText( + classification + ? formatCompactReadCall(classification, args, theme) + : formatReadCall(args, theme), + ); + return text; + }, + renderResult(result, options, theme, context) { + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + text.setText( + formatReadResult( + context.args, + result, + options, + theme, + context.showImages, + context.cwd, + context.isError, + ), + ); + return text; + }, + }; +} + +export function createReadTool( + cwd: string, + options?: ReadToolOptions, +): AgentTool { + return wrapToolDefinition(createReadToolDefinition(cwd, options)); +} diff --git a/src/agents/sessions/tools/render-utils.ts b/src/agents/sessions/tools/render-utils.ts new file mode 100644 index 00000000000..1b2021f55ef --- /dev/null +++ b/src/agents/sessions/tools/render-utils.ts @@ -0,0 +1,79 @@ +import * as os from "node:os"; +import { getCapabilities, getImageDimensions, imageFallback } from "@earendil-works/pi-tui"; +import type { ImageContent, TextContent } from "openclaw/plugin-sdk/llm"; +import type { Theme } from "../../modes/interactive/theme/theme.js"; +import { stripAnsi } from "../../utils/ansi.js"; +import { sanitizeBinaryOutput } from "../../utils/shell.js"; + +export function shortenPath(path: unknown): string { + if (typeof path !== "string") { + return ""; + } + const home = os.homedir(); + if (path.startsWith(home)) { + return `~${path.slice(home.length)}`; + } + return path; +} + +export function str(value: unknown): string | null { + if (typeof value === "string") { + return value; + } + if (value == null) { + return ""; + } + return null; +} + +export function replaceTabs(text: string): string { + return text.replace(/\t/g, " "); +} + +export function normalizeDisplayText(text: string): string { + return text.replace(/\r/g, ""); +} + +export function getTextOutput( + result: + | { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> } + | undefined, + showImages: boolean, +): string { + if (!result) { + return ""; + } + + const textBlocks = result.content.filter((c) => c.type === "text"); + const imageBlocks = result.content.filter((c) => c.type === "image"); + + let output = textBlocks + .map((c) => sanitizeBinaryOutput(stripAnsi(c.text || "")).replace(/\r/g, "")) + .join("\n"); + + const caps = getCapabilities(); + if (imageBlocks.length > 0 && (!caps.images || !showImages)) { + const imageIndicators = imageBlocks + .map((img) => { + const mimeType = img.mimeType ?? "image/unknown"; + const dims = + img.data && img.mimeType + ? (getImageDimensions(img.data, img.mimeType) ?? undefined) + : undefined; + return imageFallback(mimeType, dims); + }) + .join("\n"); + output = output ? `${output}\n${imageIndicators}` : imageIndicators; + } + + return output; +} + +export type ToolRenderResultLike = { + content: (TextContent | ImageContent)[]; + details: TDetails; +}; + +export function invalidArgText(theme: Pick): string { + return theme.fg("error", "[invalid arg]"); +} diff --git a/src/agents/sessions/tools/tool-definition-wrapper.ts b/src/agents/sessions/tools/tool-definition-wrapper.ts new file mode 100644 index 00000000000..ad5f02670eb --- /dev/null +++ b/src/agents/sessions/tools/tool-definition-wrapper.ts @@ -0,0 +1,51 @@ +import type { TSchema } from "typebox"; +import type { AgentTool } from "../../runtime/index.js"; +import type { ExtensionContext, ToolDefinition } from "../extensions/types.js"; + +/** Wrap a ToolDefinition into an AgentTool for the core runtime. */ +export function wrapToolDefinition< + TParams extends TSchema = TSchema, + TDetails = unknown, + TState = unknown, +>( + definition: ToolDefinition, + ctxFactory?: () => ExtensionContext, +): AgentTool { + return { + name: definition.name, + label: definition.label, + description: definition.description, + parameters: definition.parameters, + prepareArguments: definition.prepareArguments, + executionMode: definition.executionMode, + execute: (toolCallId, params, signal, onUpdate) => + definition.execute(toolCallId, params, signal, onUpdate, ctxFactory?.() as ExtensionContext), + }; +} + +/** Wrap multiple ToolDefinitions into AgentTools for the core runtime. */ +export function wrapToolDefinitions( + definitions: ToolDefinition[], + ctxFactory?: () => ExtensionContext, +): AgentTool[] { + return definitions.map((definition) => wrapToolDefinition(definition, ctxFactory)); +} + +/** + * Synthesize a minimal ToolDefinition from an AgentTool. + * + * This keeps AgentSession's internal registry definition-first even when a caller + * provides plain AgentTool overrides that do not include prompt metadata or renderers. + */ +export function createToolDefinitionFromAgentTool(tool: AgentTool): ToolDefinition { + return { + name: tool.name, + label: tool.label, + description: tool.description, + parameters: tool.parameters, + prepareArguments: tool.prepareArguments, + executionMode: tool.executionMode, + execute: async (toolCallId, params, signal, onUpdate) => + tool.execute(toolCallId, params, signal, onUpdate), + }; +} diff --git a/src/agents/sessions/tools/truncate.ts b/src/agents/sessions/tools/truncate.ts new file mode 100644 index 00000000000..d0b172528ef --- /dev/null +++ b/src/agents/sessions/tools/truncate.ts @@ -0,0 +1,275 @@ +/** + * Shared truncation utilities for tool outputs. + * + * Truncation is based on two independent limits - whichever is hit first wins: + * - Line limit (default: 2000 lines) + * - Byte limit (default: 50KB) + * + * Never returns partial lines (except bash tail truncation edge case). + */ + +export const DEFAULT_MAX_LINES = 2000; +export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB +export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line + +export interface TruncationResult { + /** The truncated content */ + content: string; + /** Whether truncation occurred */ + truncated: boolean; + /** Which limit was hit: "lines", "bytes", or null if not truncated */ + truncatedBy: "lines" | "bytes" | null; + /** Total number of lines in the original content */ + totalLines: number; + /** Total number of bytes in the original content */ + totalBytes: number; + /** Number of complete lines in the truncated output */ + outputLines: number; + /** Number of bytes in the truncated output */ + outputBytes: number; + /** Whether the last line was partially truncated (only for tail truncation edge case) */ + lastLinePartial: boolean; + /** Whether the first line exceeded the byte limit (for head truncation) */ + firstLineExceedsLimit: boolean; + /** The max lines limit that was applied */ + maxLines: number; + /** The max bytes limit that was applied */ + maxBytes: number; +} + +export interface TruncationOptions { + /** Maximum number of lines (default: 2000) */ + maxLines?: number; + /** Maximum number of bytes (default: 50KB) */ + maxBytes?: number; +} + +function splitLinesForCounting(content: string): string[] { + if (content.length === 0) { + return []; + } + const lines = content.split("\n"); + if (content.endsWith("\n")) { + lines.pop(); + } + return lines; +} + +/** + * Format bytes as human-readable size. + */ +export function formatSize(bytes: number): string { + if (bytes < 1024) { + return `${bytes}B`; + } else if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(1)}KB`; + } + return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; +} + +/** + * Truncate content from the head (keep first N lines/bytes). + * Suitable for file reads where you want to see the beginning. + * + * Never returns partial lines. If first line exceeds byte limit, + * returns empty content with firstLineExceedsLimit=true. + */ +export function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult { + const maxLines = options.maxLines ?? DEFAULT_MAX_LINES; + const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; + + const totalBytes = Buffer.byteLength(content, "utf-8"); + const lines = splitLinesForCounting(content); + const totalLines = lines.length; + + // Check if no truncation needed + if (totalLines <= maxLines && totalBytes <= maxBytes) { + return { + content, + truncated: false, + truncatedBy: null, + totalLines, + totalBytes, + outputLines: totalLines, + outputBytes: totalBytes, + lastLinePartial: false, + firstLineExceedsLimit: false, + maxLines, + maxBytes, + }; + } + + // Check if first line alone exceeds byte limit + const firstLineBytes = Buffer.byteLength(lines[0], "utf-8"); + if (firstLineBytes > maxBytes) { + return { + content: "", + truncated: true, + truncatedBy: "bytes", + totalLines, + totalBytes, + outputLines: 0, + outputBytes: 0, + lastLinePartial: false, + firstLineExceedsLimit: true, + maxLines, + maxBytes, + }; + } + + // Collect complete lines that fit + const outputLinesArr: string[] = []; + let outputBytesCount = 0; + let truncatedBy: "lines" | "bytes" = "lines"; + + for (let i = 0; i < lines.length && i < maxLines; i++) { + const line = lines[i]; + const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0); // +1 for newline + + if (outputBytesCount + lineBytes > maxBytes) { + truncatedBy = "bytes"; + break; + } + + outputLinesArr.push(line); + outputBytesCount += lineBytes; + } + + // If we exited due to line limit + if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) { + truncatedBy = "lines"; + } + + const outputContent = outputLinesArr.join("\n"); + const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8"); + + return { + content: outputContent, + truncated: true, + truncatedBy, + totalLines, + totalBytes, + outputLines: outputLinesArr.length, + outputBytes: finalOutputBytes, + lastLinePartial: false, + firstLineExceedsLimit: false, + maxLines, + maxBytes, + }; +} + +/** + * Truncate content from the tail (keep last N lines/bytes). + * Suitable for bash output where you want to see the end (errors, final results). + * + * May return partial first line if the last line of original content exceeds byte limit. + */ +export function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult { + const maxLines = options.maxLines ?? DEFAULT_MAX_LINES; + const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; + + const totalBytes = Buffer.byteLength(content, "utf-8"); + const lines = splitLinesForCounting(content); + const totalLines = lines.length; + + // Check if no truncation needed + if (totalLines <= maxLines && totalBytes <= maxBytes) { + return { + content, + truncated: false, + truncatedBy: null, + totalLines, + totalBytes, + outputLines: totalLines, + outputBytes: totalBytes, + lastLinePartial: false, + firstLineExceedsLimit: false, + maxLines, + maxBytes, + }; + } + + // Work backwards from the end + const outputLinesArr: string[] = []; + let outputBytesCount = 0; + let truncatedBy: "lines" | "bytes" = "lines"; + let lastLinePartial = false; + + for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) { + const line = lines[i]; + const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline + + if (outputBytesCount + lineBytes > maxBytes) { + truncatedBy = "bytes"; + // Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes, + // take the end of the line (partial) + if (outputLinesArr.length === 0) { + const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes); + outputLinesArr.unshift(truncatedLine); + outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8"); + lastLinePartial = true; + } + break; + } + + outputLinesArr.unshift(line); + outputBytesCount += lineBytes; + } + + // If we exited due to line limit + if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) { + truncatedBy = "lines"; + } + + const outputContent = outputLinesArr.join("\n"); + const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8"); + + return { + content: outputContent, + truncated: true, + truncatedBy, + totalLines, + totalBytes, + outputLines: outputLinesArr.length, + outputBytes: finalOutputBytes, + lastLinePartial, + firstLineExceedsLimit: false, + maxLines, + maxBytes, + }; +} + +/** + * Truncate a string to fit within a byte limit (from the end). + * Handles multi-byte UTF-8 characters correctly. + */ +function truncateStringToBytesFromEnd(str: string, maxBytes: number): string { + const buf = Buffer.from(str, "utf-8"); + if (buf.length <= maxBytes) { + return str; + } + + // Start from the end, skip maxBytes back + let start = buf.length - maxBytes; + + // Find a valid UTF-8 boundary (start of a character) + while (start < buf.length && (buf[start] & 0xc0) === 0x80) { + start++; + } + + return buf.slice(start).toString("utf-8"); +} + +/** + * Truncate a single line to max characters, adding [truncated] suffix. + * Used for grep match lines. + */ +export function truncateLine( + line: string, + maxChars: number = GREP_MAX_LINE_LENGTH, +): { text: string; wasTruncated: boolean } { + if (line.length <= maxChars) { + return { text: line, wasTruncated: false }; + } + return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true }; +} diff --git a/src/agents/sessions/tools/write.ts b/src/agents/sessions/tools/write.ts new file mode 100644 index 00000000000..3bc18fd8519 --- /dev/null +++ b/src/agents/sessions/tools/write.ts @@ -0,0 +1,330 @@ +import { mkdir as fsMkdir, writeFile as fsWriteFile } from "node:fs/promises"; +import { dirname } from "node:path"; +import { Container, Text } from "@earendil-works/pi-tui"; +import { type Static, Type } from "typebox"; +import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; +import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js"; +import type { AgentTool } from "../../runtime/index.js"; +import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js"; +import { withFileMutationQueue } from "./file-mutation-queue.js"; +import { resolveToCwd } from "./path-utils.js"; +import { + invalidArgText, + normalizeDisplayText, + replaceTabs, + shortenPath, + str, +} from "./render-utils.js"; +import { wrapToolDefinition } from "./tool-definition-wrapper.js"; + +const writeSchema = Type.Object({ + path: Type.String({ description: "Path to the file to write (relative or absolute)" }), + content: Type.String({ description: "Content to write to the file" }), +}); + +export type WriteToolInput = Static; + +/** + * Pluggable operations for the write tool. + * Override these to delegate file writing to remote systems (for example SSH). + */ +export interface WriteOperations { + /** Write content to a file */ + writeFile: (absolutePath: string, content: string) => Promise; + /** Create directory recursively */ + mkdir: (dir: string) => Promise; +} + +const defaultWriteOperations: WriteOperations = { + writeFile: (path, content) => fsWriteFile(path, content, "utf-8"), + mkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}), +}; + +export interface WriteToolOptions { + /** Custom operations for file writing. Default: local filesystem */ + operations?: WriteOperations; +} + +type WriteHighlightCache = { + rawPath: string | null; + lang: string; + rawContent: string; + normalizedLines: string[]; + highlightedLines: string[]; +}; + +class WriteCallRenderComponent extends Text { + cache?: WriteHighlightCache; + + constructor() { + super("", 0, 0); + } +} + +const WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50; + +function highlightSingleLine(line: string, lang: string): string { + const highlighted = highlightCode(line, lang); + return highlighted[0] ?? ""; +} + +function refreshWriteHighlightPrefix(cache: WriteHighlightCache): void { + const prefixCount = Math.min(WRITE_PARTIAL_FULL_HIGHLIGHT_LINES, cache.normalizedLines.length); + if (prefixCount === 0) { + return; + } + const prefixSource = cache.normalizedLines.slice(0, prefixCount).join("\n"); + const prefixHighlighted = highlightCode(prefixSource, cache.lang); + for (let i = 0; i < prefixCount; i++) { + cache.highlightedLines[i] = + prefixHighlighted[i] ?? highlightSingleLine(cache.normalizedLines[i] ?? "", cache.lang); + } +} + +function rebuildWriteHighlightCacheFull( + rawPath: string | null, + fileContent: string, +): WriteHighlightCache | undefined { + const lang = rawPath ? getLanguageFromPath(rawPath) : undefined; + if (!lang) { + return undefined; + } + const displayContent = normalizeDisplayText(fileContent); + const normalized = replaceTabs(displayContent); + return { + rawPath, + lang, + rawContent: fileContent, + normalizedLines: normalized.split("\n"), + highlightedLines: highlightCode(normalized, lang), + }; +} + +function updateWriteHighlightCacheIncremental( + cache: WriteHighlightCache | undefined, + rawPath: string | null, + fileContent: string, +): WriteHighlightCache | undefined { + const lang = rawPath ? getLanguageFromPath(rawPath) : undefined; + if (!lang) { + return undefined; + } + if (!cache) { + return rebuildWriteHighlightCacheFull(rawPath, fileContent); + } + if (cache.lang !== lang || cache.rawPath !== rawPath) { + return rebuildWriteHighlightCacheFull(rawPath, fileContent); + } + if (!fileContent.startsWith(cache.rawContent)) { + return rebuildWriteHighlightCacheFull(rawPath, fileContent); + } + if (fileContent.length === cache.rawContent.length) { + return cache; + } + + const deltaRaw = fileContent.slice(cache.rawContent.length); + const deltaDisplay = normalizeDisplayText(deltaRaw); + const deltaNormalized = replaceTabs(deltaDisplay); + cache.rawContent = fileContent; + if (cache.normalizedLines.length === 0) { + cache.normalizedLines.push(""); + cache.highlightedLines.push(""); + } + + const segments = deltaNormalized.split("\n"); + const lastIndex = cache.normalizedLines.length - 1; + cache.normalizedLines[lastIndex] += segments[0]; + cache.highlightedLines[lastIndex] = highlightSingleLine( + cache.normalizedLines[lastIndex], + cache.lang, + ); + for (let i = 1; i < segments.length; i++) { + cache.normalizedLines.push(segments[i]); + cache.highlightedLines.push(highlightSingleLine(segments[i], cache.lang)); + } + refreshWriteHighlightPrefix(cache); + return cache; +} + +function trimTrailingEmptyLines(lines: string[]): string[] { + let end = lines.length; + while (end > 0 && lines[end - 1] === "") { + end--; + } + return lines.slice(0, end); +} + +function formatWriteCall( + args: { path?: string; file_path?: string; content?: string } | undefined, + options: ToolRenderResultOptions, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, + cache: WriteHighlightCache | undefined, +): string { + const rawPath = str(args?.file_path ?? args?.path); + const fileContent = str(args?.content); + const path = rawPath !== null ? shortenPath(rawPath) : null; + const invalidArg = invalidArgText(theme); + let text = `${theme.fg("toolTitle", theme.bold("write"))} ${path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "...")}`; + + if (fileContent === null) { + text += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`; + } else if (fileContent) { + const lang = rawPath ? getLanguageFromPath(rawPath) : undefined; + const renderedLines = lang + ? (cache?.highlightedLines ?? + highlightCode(replaceTabs(normalizeDisplayText(fileContent)), lang)) + : normalizeDisplayText(fileContent).split("\n"); + const lines = trimTrailingEmptyLines(renderedLines); + const totalLines = lines.length; + const maxLines = options.expanded ? lines.length : 10; + const displayLines = lines.slice(0, maxLines); + const remaining = lines.length - maxLines; + text += `\n\n${displayLines.map((line) => (lang ? line : theme.fg("toolOutput", replaceTabs(line)))).join("\n")}`; + if (remaining > 0) { + text += `${theme.fg("muted", `\n... (${remaining} more lines, ${totalLines} total,`)} ${keyHint("app.tools.expand", "to expand")})`; + } + } + + return text; +} + +function formatWriteResult( + result: { + content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; + isError?: boolean; + }, + theme: typeof import("../../modes/interactive/theme/theme.js").theme, +): string | undefined { + if (!result.isError) { + return undefined; + } + const output = result.content + .filter((c) => c.type === "text") + .map((c) => c.text || "") + .join("\n"); + if (!output) { + return undefined; + } + return `\n${theme.fg("error", output)}`; +} + +export function createWriteToolDefinition( + cwd: string, + options?: WriteToolOptions, +): ToolDefinition { + const ops = options?.operations ?? defaultWriteOperations; + return { + name: "write", + label: "write", + description: + "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.", + promptSnippet: "Create or overwrite files", + promptGuidelines: ["Use write only for new files or complete rewrites."], + parameters: writeSchema, + async execute( + toolCallId, + { path, content }: { path: string; content: string }, + signal?: AbortSignal, + onUpdate?, + ctx?, + ) { + void toolCallId; + void onUpdate; + void ctx; + const absolutePath = resolveToCwd(path, cwd); + const dir = dirname(absolutePath); + return withFileMutationQueue( + absolutePath, + () => + new Promise<{ content: Array<{ type: "text"; text: string }>; details: undefined }>( + (resolve, reject) => { + if (signal?.aborted) { + reject(new Error("Operation aborted")); + return; + } + let aborted = false; + const onAbort = () => { + aborted = true; + reject(new Error("Operation aborted")); + }; + signal?.addEventListener("abort", onAbort, { once: true }); + void (async () => { + try { + // Create parent directories if needed. + await ops.mkdir(dir); + if (aborted) { + return; + } + // Write the file contents. + await ops.writeFile(absolutePath, content); + if (aborted) { + return; + } + signal?.removeEventListener("abort", onAbort); + resolve({ + content: [ + { + type: "text", + text: `Successfully wrote ${content.length} bytes to ${path}`, + }, + ], + details: undefined, + }); + } catch (error: unknown) { + signal?.removeEventListener("abort", onAbort); + if (!aborted) { + reject(error); + } + } + })(); + }, + ), + ); + }, + renderCall(args, theme, context) { + const renderArgs = args as + | { path?: string; file_path?: string; content?: string } + | undefined; + const rawPath = str(renderArgs?.file_path ?? renderArgs?.path); + const fileContent = str(renderArgs?.content); + const component = + (context.lastComponent as WriteCallRenderComponent | undefined) ?? + new WriteCallRenderComponent(); + if (fileContent !== null) { + component.cache = context.argsComplete + ? rebuildWriteHighlightCacheFull(rawPath, fileContent) + : updateWriteHighlightCacheIncremental(component.cache, rawPath, fileContent); + } else { + component.cache = undefined; + } + component.setText( + formatWriteCall( + renderArgs, + { expanded: context.expanded, isPartial: context.isPartial }, + theme, + component.cache, + ), + ); + return component; + }, + renderResult(result, options, theme, context) { + void options; + const output = formatWriteResult({ ...result, isError: context.isError }, theme); + if (!output) { + const component = (context.lastComponent as Container | undefined) ?? new Container(); + component.clear(); + return component; + } + const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0); + text.setText(output); + return text; + }, + }; +} + +export function createWriteTool( + cwd: string, + options?: WriteToolOptions, +): AgentTool { + return wrapToolDefinition(createWriteToolDefinition(cwd, options)); +} diff --git a/src/agents/simple-completion-runtime.test.ts b/src/agents/simple-completion-runtime.test.ts index 09a54e0d110..c69db1e287e 100644 --- a/src/agents/simple-completion-runtime.test.ts +++ b/src/agents/simple-completion-runtime.test.ts @@ -1,5 +1,5 @@ -import type { Model } from "@earendil-works/pi-ai"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { Model } from "openclaw/plugin-sdk/llm"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; const hoisted = vi.hoisted(() => ({ @@ -14,11 +14,11 @@ const hoisted = vi.hoisted(() => ({ completeMock: vi.fn(), })); -vi.mock("@earendil-works/pi-ai", () => ({ +vi.mock("openclaw/plugin-sdk/llm", () => ({ completeSimple: hoisted.completeMock, })); -vi.mock("./pi-embedded-runner/model.js", () => ({ +vi.mock("./embedded-agent-runner/model.js", () => ({ resolveModel: hoisted.resolveModelMock, resolveModelAsync: hoisted.resolveModelAsyncMock, })); @@ -433,7 +433,7 @@ describe("prepareSimpleCompletionModel", () => { expect(result.auth.apiKey).toBe("bedrock-runtime-token"); }); - it("can skip Pi model/auth discovery for config-scoped one-shot completions", async () => { + it("can skip agent model/auth discovery for config-scoped one-shot completions", async () => { hoisted.resolveModelAsyncMock.mockResolvedValueOnce({ model: { provider: "ollama", @@ -454,7 +454,7 @@ describe("prepareSimpleCompletionModel", () => { cfg: undefined, provider: "ollama", modelId: "llama3.2:latest", - skipPiDiscovery: true, + skipAgentDiscovery: true, modelResolver: hoisted.resolveModelAsyncMock, }); @@ -466,7 +466,7 @@ describe("prepareSimpleCompletionModel", () => { undefined, undefined, { - skipPiDiscovery: true, + skipAgentDiscovery: true, }, ); }); @@ -488,7 +488,7 @@ describe("prepareSimpleCompletionModel", () => { provider: "mistral", modelId: "mistral-medium-3-5", allowBundledStaticCatalogFallback: true, - skipPiDiscovery: true, + skipAgentDiscovery: true, modelResolver: hoisted.resolveModelAsyncMock, }); @@ -500,7 +500,7 @@ describe("prepareSimpleCompletionModel", () => { undefined, { allowBundledStaticCatalogFallback: true, - skipPiDiscovery: true, + skipAgentDiscovery: true, }, ); }); @@ -532,7 +532,7 @@ describe("prepareSimpleCompletionModelForAgent", () => { const result = await prepareSimpleCompletionModelForAgent({ cfg, agentId: "main", - skipPiDiscovery: true, + skipAgentDiscovery: true, modelResolver: hoisted.resolveModelAsyncMock, }); @@ -546,7 +546,7 @@ describe("prepareSimpleCompletionModelForAgent", () => { expect.any(String), cfg, { - skipPiDiscovery: true, + skipAgentDiscovery: true, }, ); expect( @@ -603,7 +603,7 @@ describe("completeWithPreparedSimpleCompletionModel", () => { ); }); - it("normalizes OpenClaw-only thinking levels before using pi-ai simple completion", async () => { + it("normalizes OpenClaw-only thinking levels before using shared model runtime simple completion", async () => { const model = { provider: "openai", id: "gpt-5.4", diff --git a/src/agents/simple-completion-runtime.ts b/src/agents/simple-completion-runtime.ts index 92f90b316b1..6dce96dc3b5 100644 --- a/src/agents/simple-completion-runtime.ts +++ b/src/agents/simple-completion-runtime.ts @@ -1,15 +1,15 @@ import { completeSimple, - type Api, type Model, type ThinkingLevel as SimpleCompletionThinkingLevel, -} from "@earendil-works/pi-ai"; +} from "openclaw/plugin-sdk/llm"; import type { ThinkLevel } from "../auto-reply/thinking.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { formatErrorMessage } from "../infra/errors.js"; import { prepareProviderRuntimeAuth } from "../plugins/provider-runtime.runtime.js"; import { resolveAgentDir, resolveAgentEffectiveModelPrimary } from "./agent-scope.js"; import { DEFAULT_PROVIDER } from "./defaults.js"; +import { resolveModel, resolveModelAsync } from "./embedded-agent-runner/model.js"; import { resolveAgentHarnessPolicy } from "./harness/policy.js"; import { applyLocalNoAuthHeaderOverride, @@ -24,7 +24,6 @@ import { resolveModelRefFromString, } from "./model-selection.js"; import { OPENAI_CODEX_PROVIDER_ID, isOpenAIProvider } from "./openai-codex-routing.js"; -import { resolveModel, resolveModelAsync } from "./pi-embedded-runner/model.js"; import { prepareModelForSimpleCompletion } from "./simple-completion-transport.js"; type SimpleCompletionAuthStorage = { @@ -47,7 +46,7 @@ export type SimpleCompletionModelOptions = { export type PreparedSimpleCompletionModel = | { - model: Model; + model: Model; auth: ResolvedProviderAuth; } | { @@ -67,7 +66,7 @@ export type AgentSimpleCompletionSelection = { export type PreparedSimpleCompletionModelForAgent = | { selection: AgentSimpleCompletionSelection; - model: Model; + model: Model; auth: ResolvedProviderAuth; } | { @@ -138,7 +137,7 @@ function resolveSimpleCompletionRuntimeProvider(params: { async function setRuntimeApiKeyForCompletion(params: { authStorage: SimpleCompletionAuthStorage; - model: Model; + model: Model; apiKey: string; authMode: ResolvedProviderAuth["mode"]; cfg?: OpenClawConfig; @@ -197,10 +196,10 @@ export async function prepareSimpleCompletionModel(params: { preferredProfile?: string; allowMissingApiKeyModes?: ReadonlyArray; allowBundledStaticCatalogFallback?: boolean; - skipPiDiscovery?: boolean; + skipAgentDiscovery?: boolean; modelResolver?: typeof resolveModelAsync; }): Promise { - const resolved = params.skipPiDiscovery + const resolved = params.skipAgentDiscovery ? await (params.modelResolver ?? resolveModelAsync)( params.provider, params.modelId, @@ -210,7 +209,7 @@ export async function prepareSimpleCompletionModel(params: { ...(params.allowBundledStaticCatalogFallback !== undefined ? { allowBundledStaticCatalogFallback: params.allowBundledStaticCatalogFallback } : {}), - skipPiDiscovery: true, + skipAgentDiscovery: true, }, ) : resolveModel(params.provider, params.modelId, params.agentDir, params.cfg); @@ -288,7 +287,7 @@ export async function prepareSimpleCompletionModelForAgent(params: { preferredProfile?: string; allowMissingApiKeyModes?: ReadonlyArray; allowBundledStaticCatalogFallback?: boolean; - skipPiDiscovery?: boolean; + skipAgentDiscovery?: boolean; modelResolver?: typeof resolveModelAsync; }): Promise { const selection = resolveSimpleCompletionSelectionForAgent({ @@ -312,7 +311,7 @@ export async function prepareSimpleCompletionModelForAgent(params: { ...(params.allowBundledStaticCatalogFallback !== undefined ? { allowBundledStaticCatalogFallback: params.allowBundledStaticCatalogFallback } : {}), - skipPiDiscovery: params.skipPiDiscovery, + skipAgentDiscovery: params.skipAgentDiscovery, modelResolver: params.modelResolver, }); if ("error" in prepared) { @@ -329,7 +328,7 @@ export async function prepareSimpleCompletionModelForAgent(params: { } export async function completeWithPreparedSimpleCompletionModel(params: { - model: Model; + model: Model; auth: ResolvedProviderAuth; context: Parameters[1]; cfg?: OpenClawConfig; diff --git a/src/agents/simple-completion-transport.test.ts b/src/agents/simple-completion-transport.test.ts index 08e1b8b7fcf..206292d0ca8 100644 --- a/src/agents/simple-completion-transport.test.ts +++ b/src/agents/simple-completion-transport.test.ts @@ -1,4 +1,4 @@ -import type { Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; diff --git a/src/agents/simple-completion-transport.ts b/src/agents/simple-completion-transport.ts index 9461e9efdb7..77b3d0ca3c8 100644 --- a/src/agents/simple-completion-transport.ts +++ b/src/agents/simple-completion-transport.ts @@ -1,4 +1,4 @@ -import { getApiProvider, type Api, type Model } from "@earendil-works/pi-ai"; +import { getApiProvider, type Api, type Model } from "openclaw/plugin-sdk/llm"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createAnthropicVertexStreamFnForModel } from "./anthropic-vertex-stream.js"; import { ensureCustomApiRegistered } from "./custom-api-registry.js"; @@ -77,7 +77,7 @@ function prepareCodexSimpleTransportModel( export function prepareModelForSimpleCompletion(params: { model: Model; cfg?: OpenClawConfig; -}): Model { +}): Model { const { model, cfg } = params; // Only provider-owned custom APIs need runtime stream registration here. if (!getApiProvider(model.api) && registerProviderStreamForModel({ model, cfg })) { diff --git a/src/agents/skills/compact-format.test.ts b/src/agents/skills/compact-format.test.ts index 3ceb4b4553c..f8411b7cf14 100644 --- a/src/agents/skills/compact-format.test.ts +++ b/src/agents/skills/compact-format.test.ts @@ -1,5 +1,5 @@ import os from "node:os"; -import { formatSkillsForPrompt as upstreamFormatSkillsForPrompt } from "@earendil-works/pi-coding-agent"; +import { formatSkillsForPrompt as upstreamFormatSkillsForPrompt } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import { createCanonicalFixtureSkill } from "../skills.test-helpers.js"; diff --git a/src/agents/skills/skill-contract.ts b/src/agents/skills/skill-contract.ts index 11f1a024a7c..dfa76f900cd 100644 --- a/src/agents/skills/skill-contract.ts +++ b/src/agents/skills/skill-contract.ts @@ -1,4 +1,4 @@ -import type { Skill as CanonicalSkill, SourceInfo } from "@earendil-works/pi-coding-agent"; +import type { Skill as CanonicalSkill, SourceInfo } from "../sessions/index.js"; export type SourceScope = "user" | "project" | "temporary"; export type SourceOrigin = "package" | "top-level"; @@ -37,7 +37,7 @@ function escapeXml(str: string): string { /** * Keep this formatter's XML layout byte-for-byte aligned with the upstream - * Agent Skills formatter so we can avoid importing the full pi-coding-agent + * Agent Skills formatter so we can avoid importing the full session runtime * package root on the cold skills path. Visibility policy is applied upstream * before calling this helper. */ diff --git a/src/agents/stream-message-shared.ts b/src/agents/stream-message-shared.ts index e669d26d08e..e4e0832c667 100644 --- a/src/agents/stream-message-shared.ts +++ b/src/agents/stream-message-shared.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage, StopReason, Usage } from "@earendil-works/pi-ai"; +import type { AssistantMessage, StopReason, Usage } from "openclaw/plugin-sdk/llm"; type StreamModelDescriptor = { api: string; diff --git a/src/agents/subagent-announce-delivery.runtime.ts b/src/agents/subagent-announce-delivery.runtime.ts index cf25f43d1fc..fe7df3c2044 100644 --- a/src/agents/subagent-announce-delivery.runtime.ts +++ b/src/agents/subagent-announce-delivery.runtime.ts @@ -13,8 +13,8 @@ export { createBoundDeliveryRouter } from "../infra/outbound/bound-delivery-rout export { resolveConversationIdFromTargets } from "../infra/outbound/conversation-id.js"; export { getGlobalHookRunner } from "../plugins/hook-runner-global.js"; export { - formatEmbeddedPiQueueFailureSummary, - isEmbeddedPiRunActive, - queueEmbeddedPiMessageWithOutcomeAsync, + formatEmbeddedAgentQueueFailureSummary, + isEmbeddedAgentRunActive, + queueEmbeddedAgentMessageWithOutcomeAsync, resolveActiveEmbeddedRunSessionId, -} from "./pi-embedded-runner/runs.js"; +} from "./embedded-agent-runner/runs.js"; diff --git a/src/agents/subagent-announce-delivery.test.ts b/src/agents/subagent-announce-delivery.test.ts index 3679e9af777..5d870c55842 100644 --- a/src/agents/subagent-announce-delivery.test.ts +++ b/src/agents/subagent-announce-delivery.test.ts @@ -5,12 +5,12 @@ import { } from "../infra/outbound/session-binding-service.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js"; -import type { AgentInternalEvent } from "./internal-events.js"; import type { - EmbeddedPiQueueFailureReason, - EmbeddedPiQueueMessageOptions, - EmbeddedPiQueueMessageOutcome, -} from "./pi-embedded-runner/runs.js"; + EmbeddedAgentQueueFailureReason, + EmbeddedAgentQueueMessageOptions, + EmbeddedAgentQueueMessageOutcome, +} from "./embedded-agent-runner/runs.js"; +import type { AgentInternalEvent } from "./internal-events.js"; import { testing, deliverSubagentAnnouncement, @@ -65,15 +65,15 @@ function createSendMessageMock() { })) as unknown as typeof runtimeSendMessage; } -type QueueEmbeddedPiMessageWithOutcome = ( +type QueueEmbeddedAgentMessageWithOutcome = ( sessionId: string, message: string, - options?: EmbeddedPiQueueMessageOptions, -) => EmbeddedPiQueueMessageOutcome; + options?: EmbeddedAgentQueueMessageOptions, +) => EmbeddedAgentQueueMessageOutcome; function createQueueOutcomeMock( queued: boolean, -): ReturnType> { +): ReturnType> { return vi.fn((sessionId: string) => queued ? { @@ -94,8 +94,8 @@ function createQueueOutcomeMock( } function createQueueOutcomeSequenceMock( - queuedOutcomes: (boolean | EmbeddedPiQueueFailureReason)[], -): ReturnType> { + queuedOutcomes: (boolean | EmbeddedAgentQueueFailureReason)[], +): ReturnType> { let index = 0; return vi.fn((sessionId: string) => { const outcome = queuedOutcomes[Math.min(index, queuedOutcomes.length - 1)] ?? false; @@ -191,7 +191,7 @@ async function deliverSlackThreadAnnouncement(params: { sessionId: string; expectsCompletionMessage: boolean; directIdempotencyKey: string; - queueEmbeddedPiMessageWithOutcome?: QueueEmbeddedPiMessageWithOutcome; + queueEmbeddedAgentMessageWithOutcome?: QueueEmbeddedAgentMessageWithOutcome; sendMessage?: typeof runtimeSendMessage; internalEvents?: AgentInternalEvent[]; sourceTool?: string; @@ -204,8 +204,8 @@ async function deliverSlackThreadAnnouncement(params: { }), getRuntimeConfig: () => ({}) as never, sendMessage: params.sendMessage ?? runtimeSendMessage, - ...(params.queueEmbeddedPiMessageWithOutcome - ? { queueEmbeddedPiMessageWithOutcome: params.queueEmbeddedPiMessageWithOutcome } + ...(params.queueEmbeddedAgentMessageWithOutcome + ? { queueEmbeddedAgentMessageWithOutcome: params.queueEmbeddedAgentMessageWithOutcome } : {}), }); @@ -272,7 +272,7 @@ async function deliverTelegramDirectMessageCompletion(params: { internalEvents?: AgentInternalEvent[]; isActive?: boolean; requesterSessionId?: string | null; - queueEmbeddedPiMessageWithOutcome?: QueueEmbeddedPiMessageWithOutcome; + queueEmbeddedAgentMessageWithOutcome?: QueueEmbeddedAgentMessageWithOutcome; requesterSessionKey?: string; sourceTool?: string; runtimeConfig?: Record; @@ -300,8 +300,8 @@ async function deliverTelegramDirectMessageCompletion(params: { }), getRuntimeConfig: () => (params.runtimeConfig ?? {}) as never, sendMessage: params.sendMessage ?? runtimeSendMessage, - ...(params.queueEmbeddedPiMessageWithOutcome - ? { queueEmbeddedPiMessageWithOutcome: params.queueEmbeddedPiMessageWithOutcome } + ...(params.queueEmbeddedAgentMessageWithOutcome + ? { queueEmbeddedAgentMessageWithOutcome: params.queueEmbeddedAgentMessageWithOutcome } : {}), }); @@ -342,7 +342,7 @@ async function deliverSlackChannelAnnouncement(params: { accountId?: string; threadId?: string | number; }; - queueEmbeddedPiMessageWithOutcome?: QueueEmbeddedPiMessageWithOutcome; + queueEmbeddedAgentMessageWithOutcome?: QueueEmbeddedAgentMessageWithOutcome; sendMessage?: typeof runtimeSendMessage; internalEvents?: AgentInternalEvent[]; sourceTool?: string; @@ -362,8 +362,8 @@ async function deliverSlackChannelAnnouncement(params: { }), getRuntimeConfig: () => (params.runtimeConfig ?? {}) as never, sendMessage: params.sendMessage ?? runtimeSendMessage, - ...(params.queueEmbeddedPiMessageWithOutcome - ? { queueEmbeddedPiMessageWithOutcome: params.queueEmbeddedPiMessageWithOutcome } + ...(params.queueEmbeddedAgentMessageWithOutcome + ? { queueEmbeddedAgentMessageWithOutcome: params.queueEmbeddedAgentMessageWithOutcome } : {}), }); @@ -626,7 +626,7 @@ describe("resolveSubagentCompletionOrigin", () => { describe("deliverSubagentAnnouncement active requester steering", () => { async function deliverSteeredAnnouncement(params: { mode?: "followup" | "collect" | "interrupt"; - queueEmbeddedPiMessageWithOutcome?: QueueEmbeddedPiMessageWithOutcome; + queueEmbeddedAgentMessageWithOutcome?: QueueEmbeddedAgentMessageWithOutcome; requesterOrigin?: { channel?: string; to?: string; @@ -642,8 +642,8 @@ describe("deliverSubagentAnnouncement active requester steering", () => { sessionId: "paperclip-session", isActive: activityChecks++ === 0, }), - queueEmbeddedPiMessageWithOutcome: - params.queueEmbeddedPiMessageWithOutcome ?? createQueueOutcomeMock(true), + queueEmbeddedAgentMessageWithOutcome: + params.queueEmbeddedAgentMessageWithOutcome ?? createQueueOutcomeMock(true), getRuntimeConfig: () => ({ messages: { @@ -718,10 +718,10 @@ describe("deliverSubagentAnnouncement active requester steering", () => { it.each(["followup", "collect", "interrupt"] as const)( "steers active requester announces even in %s mode", async (mode) => { - const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(true); + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeMock(true); await deliverSteeredAnnouncement({ mode, - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, requesterOrigin: { channel: "slack", to: "channel:C123", @@ -729,13 +729,13 @@ describe("deliverSubagentAnnouncement active requester steering", () => { }, }); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenCalledOnce(); + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenCalledOnce(); }, ); it("preserves best-effort steering for active runtimes without transcript wait support", async () => { - const queueEmbeddedPiMessageWithOutcome = vi - .fn() + const queueEmbeddedAgentMessageWithOutcome = vi + .fn() .mockImplementationOnce((sessionId: string) => ({ queued: false, sessionId, @@ -750,7 +750,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => { enqueuedAtMs: 4_100, })); const callGateway = await deliverSteeredAnnouncement({ - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, requesterOrigin: { channel: "slack", to: "channel:C123", @@ -759,8 +759,8 @@ describe("deliverSubagentAnnouncement active requester steering", () => { }); expect(callGateway).not.toHaveBeenCalled(); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenCalledTimes(2); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenNthCalledWith( + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenCalledTimes(2); + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenNthCalledWith( 1, "paperclip-session", "child done", @@ -771,7 +771,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => { deliveryTimeoutMs: 120_000, }, ); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenNthCalledWith( + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenNthCalledWith( 2, "paperclip-session", "child done", @@ -784,7 +784,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => { }); it("does not report delivery when active requester steering is rejected", async () => { - const queueEmbeddedPiMessageWithOutcome = vi.fn(async (sessionId: string) => ({ + const queueEmbeddedAgentMessageWithOutcome = vi.fn(async (sessionId: string) => ({ queued: false as const, sessionId, reason: "runtime_rejected" as const, @@ -798,7 +798,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => { sessionId: "paperclip-session", isActive: true, }), - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, getRuntimeConfig: () => ({ messages: { @@ -829,7 +829,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => { }); it("falls through to direct delivery when requester ends during awaited steering failure", async () => { - const queueEmbeddedPiMessageWithOutcome = vi.fn(async (sessionId: string) => ({ + const queueEmbeddedAgentMessageWithOutcome = vi.fn(async (sessionId: string) => ({ queued: false as const, sessionId, reason: "runtime_rejected" as const, @@ -848,7 +848,7 @@ describe("deliverSubagentAnnouncement active requester steering", () => { sessionId: "paperclip-session", isActive: activityChecks++ === 0, }), - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, getRuntimeConfig: () => ({ messages: { @@ -886,14 +886,14 @@ describe("deliverSubagentAnnouncement active requester steering", () => { describe("deliverSubagentAnnouncement completion delivery", () => { it("uses an active requester queue as the completion handoff when message-tool delivery is not required", async () => { const callGateway = createGatewayMock(); - const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(true); + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeMock(true); const result = await deliverSlackThreadAnnouncement({ callGateway, sessionId: "requester-session-1", isActive: true, expectsCompletionMessage: true, directIdempotencyKey: "announce-1", - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, }); expectRecordFields(result, { @@ -902,7 +902,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { enqueuedAt: 4_100, deliveredAt: 4_200, }); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenCalledWith( + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenCalledWith( "requester-session-1", "child done", { @@ -917,14 +917,14 @@ describe("deliverSubagentAnnouncement completion delivery", () => { it("does not also direct-run a queued active completion", async () => { const callGateway = createGatewayMock(); - const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(true); + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeMock(true); const result = await deliverSlackThreadAnnouncement({ callGateway, sessionId: "requester-session-1", isActive: true, expectsCompletionMessage: true, directIdempotencyKey: "announce-harness-task", - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, sourceTool: "agent_harness_task", }); @@ -934,20 +934,20 @@ describe("deliverSubagentAnnouncement completion delivery", () => { enqueuedAt: 4_100, deliveredAt: 4_200, }); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenCalledTimes(1); + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenCalledTimes(1); expect(callGateway).not.toHaveBeenCalled(); }); it("keeps direct external delivery for dormant completion requesters", async () => { const callGateway = createGatewayMock(); - const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(false); + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeMock(false); await deliverSlackThreadAnnouncement({ callGateway, sessionId: "requester-session-2", isActive: false, expectsCompletionMessage: true, directIdempotencyKey: "announce-1b", - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, }); expectGatewayAgentParams(callGateway, { @@ -958,7 +958,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { threadId: "171.222", bestEffortDeliver: true, }); - expect(queueEmbeddedPiMessageWithOutcome).not.toHaveBeenCalled(); + expect(queueEmbeddedAgentMessageWithOutcome).not.toHaveBeenCalled(); }); it("directly delivers direct-message subagent text when the announce agent returns no visible output", async () => { @@ -1485,14 +1485,14 @@ describe("deliverSubagentAnnouncement completion delivery", () => { }, }); const sendMessage = createSendMessageMock(); - const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeSequenceMock([ + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeSequenceMock([ "transcript_commit_wait_unsupported", "no_active_run", ]); const result = await deliverSlackThreadAnnouncement({ callGateway, sendMessage, - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, sessionId: "requester-session-4", isActive: true, expectsCompletionMessage: true, @@ -1525,8 +1525,8 @@ describe("deliverSubagentAnnouncement completion delivery", () => { to: "channel:C123", threadId: "171.222", }); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenCalledTimes(2); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenNthCalledWith( + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenCalledTimes(2); + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenNthCalledWith( 1, "requester-session-4", "child done", @@ -1537,7 +1537,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { waitForTranscriptCommit: true, }, ); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenNthCalledWith( + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenNthCalledWith( 2, "requester-session-4", "child done", @@ -1674,7 +1674,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { const result = await deliverTelegramDirectMessageCompletion({ callGateway, sendMessage, - queueEmbeddedPiMessageWithOutcome: createQueueOutcomeMock(false), + queueEmbeddedAgentMessageWithOutcome: createQueueOutcomeMock(false), requesterSessionId: null, requesterSessionKey: "agent:main:telegram:direct:123456789", origin: { @@ -1722,12 +1722,11 @@ describe("deliverSubagentAnnouncement completion delivery", () => { }, }); const sendMessage = createSendMessageMock(); - const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(false); + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeMock(false); const result = await deliverTelegramDirectMessageCompletion({ callGateway, sendMessage, isActive: true, - queueEmbeddedPiMessageWithOutcome, runtimeConfig: { agents: { defaults: { @@ -1737,6 +1736,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { }, }, }, + queueEmbeddedAgentMessageWithOutcome, internalEvents: [ { type: "task_completion", @@ -1765,8 +1765,8 @@ describe("deliverSubagentAnnouncement completion delivery", () => { }, ], }); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenCalledTimes(1); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenCalledWith( + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenCalledTimes(1); + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenCalledWith( "requester-session-telegram", "child done", { @@ -1786,8 +1786,8 @@ describe("deliverSubagentAnnouncement completion delivery", () => { payloads: [], }, }); - const queueEmbeddedPiMessageWithOutcome = vi - .fn() + const queueEmbeddedAgentMessageWithOutcome = vi + .fn() .mockImplementationOnce((sessionId: string) => ({ queued: false, sessionId, @@ -1806,7 +1806,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { isActive: true, expectsCompletionMessage: true, directIdempotencyKey: "announce-channel-empty-direct-steer-fallback", - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, internalEvents: [ { type: "task_completion", @@ -1835,7 +1835,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { }, ], }); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenCalledTimes(1); + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenCalledTimes(1); expect(callGateway).toHaveBeenCalledTimes(1); }); @@ -1846,14 +1846,14 @@ describe("deliverSubagentAnnouncement completion delivery", () => { }, }); const sendMessage = createSendMessageMock(); - const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeSequenceMock([ + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeSequenceMock([ "transcript_commit_wait_unsupported", "no_active_run", ]); const result = await deliverSlackThreadAnnouncement({ callGateway, sendMessage, - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, sessionId: "requester-session-4", isActive: true, expectsCompletionMessage: true, @@ -2509,7 +2509,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { it("keeps generated media completions on the active requester session path", async () => { const callGateway = createGatewayMock(); - const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(true); + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeMock(true); const sendMessage = createSendMessageMock(); const result = await deliverSlackChannelAnnouncement({ callGateway, @@ -2519,7 +2519,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { expectsCompletionMessage: true, directIdempotencyKey: "announce-channel-media-active-direct", sourceTool: "video_generate", - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, internalEvents: [ { type: "task_completion", @@ -2544,7 +2544,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { enqueuedAt: 4_100, deliveredAt: 4_200, }); - expect(queueEmbeddedPiMessageWithOutcome).toHaveBeenCalledWith( + expect(queueEmbeddedAgentMessageWithOutcome).toHaveBeenCalledWith( "requester-session-channel", "child done", { @@ -2966,14 +2966,14 @@ describe("deliverSubagentAnnouncement completion delivery", () => { }, }); const sendMessage = createSendMessageMock(); - const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeSequenceMock([ + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeSequenceMock([ "transcript_commit_wait_unsupported", "no_active_run", ]); const result = await deliverSlackChannelAnnouncement({ callGateway, sendMessage, - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, sessionId: "requester-session-channel", isActive: true, expectsCompletionMessage: true, @@ -3010,7 +3010,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { messagingToolSentTexts: ["The subagent is done."], }, }); - const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(false); + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeMock(false); const result = await deliverSlackChannelAnnouncement({ callGateway, sessionId: "requester-session-channel", @@ -3019,7 +3019,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { directIdempotencyKey: "announce-channel-subagent-message-tool", sourceTool: "subagent_announce", runtimeConfig: { messages: { groupChat: { visibleReplies: "message_tool" } } }, - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, internalEvents: [ { type: "task_completion", @@ -3056,7 +3056,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { payloads: [{ text: "The subagent is done." }], }, }); - const queueEmbeddedPiMessageWithOutcome = createQueueOutcomeMock(false); + const queueEmbeddedAgentMessageWithOutcome = createQueueOutcomeMock(false); const result = await deliverSlackChannelAnnouncement({ callGateway, sessionId: "requester-session-channel", @@ -3065,7 +3065,7 @@ describe("deliverSubagentAnnouncement completion delivery", () => { directIdempotencyKey: "announce-channel-subagent-message-tool-missing", sourceTool: "subagent_announce", runtimeConfig: { messages: { groupChat: { visibleReplies: "message_tool" } } }, - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, internalEvents: [ { type: "task_completion", diff --git a/src/agents/subagent-announce-delivery.ts b/src/agents/subagent-announce-delivery.ts index 70eaa31056a..297ef55a22e 100644 --- a/src/agents/subagent-announce-delivery.ts +++ b/src/agents/subagent-announce-delivery.ts @@ -25,8 +25,6 @@ import { isInternalMessageChannel, normalizeMessageChannel, } from "../utils/message-channel.js"; -import { mediaUrlsFromGeneratedAttachments } from "./generated-attachments.js"; -import type { AgentInternalEvent } from "./internal-events.js"; import { collectMessagingToolDeliveredMediaUrls, getAgentCommandDeliveryFailure, @@ -34,19 +32,21 @@ import { hasDeliveredExpectedMedia, hasMessagingToolDeliveryEvidence, hasVisibleAgentPayload, -} from "./pi-embedded-runner/delivery-evidence.js"; -import type { EmbeddedPiQueueMessageOptions } from "./pi-embedded-runner/run-state.js"; -import type { EmbeddedPiQueueMessageOutcome } from "./pi-embedded-runner/runs.js"; +} from "./embedded-agent-runner/delivery-evidence.js"; +import type { EmbeddedAgentQueueMessageOptions } from "./embedded-agent-runner/run-state.js"; +import type { EmbeddedAgentQueueMessageOutcome } from "./embedded-agent-runner/runs.js"; +import { mediaUrlsFromGeneratedAttachments } from "./generated-attachments.js"; +import type { AgentInternalEvent } from "./internal-events.js"; import { callGateway, createBoundDeliveryRouter, dispatchGatewayMethodInProcess, getGlobalHookRunner, - isEmbeddedPiRunActive, + isEmbeddedAgentRunActive, getRuntimeConfig, - formatEmbeddedPiQueueFailureSummary, + formatEmbeddedAgentQueueFailureSummary, loadSessionStore, - queueEmbeddedPiMessageWithOutcomeAsync, + queueEmbeddedAgentMessageWithOutcomeAsync, resolveActiveEmbeddedRunSessionId, resolveAgentIdFromSessionKey, resolveConversationIdFromTargets, @@ -73,11 +73,11 @@ type SubagentAnnounceDeliveryDeps = { sessionId?: string; isActive: boolean; }; - queueEmbeddedPiMessageWithOutcome: ( + queueEmbeddedAgentMessageWithOutcome: ( sessionId: string, text: string, - options?: EmbeddedPiQueueMessageOptions, - ) => EmbeddedPiQueueMessageOutcome | Promise; + options?: EmbeddedAgentQueueMessageOptions, + ) => EmbeddedAgentQueueMessageOutcome | Promise; sendMessage: typeof sendMessage; }; @@ -90,22 +90,22 @@ const defaultSubagentAnnounceDeliveryDeps: SubagentAnnounceDeliveryDeps = { loadRequesterSessionEntry(requesterSessionKey).entry?.sessionId; return { sessionId, - isActive: Boolean(sessionId && isEmbeddedPiRunActive(sessionId)), + isActive: Boolean(sessionId && isEmbeddedAgentRunActive(sessionId)), }; }, - queueEmbeddedPiMessageWithOutcome: queueEmbeddedPiMessageWithOutcomeAsync, + queueEmbeddedAgentMessageWithOutcome: queueEmbeddedAgentMessageWithOutcomeAsync, sendMessage, }; let subagentAnnounceDeliveryDeps: SubagentAnnounceDeliveryDeps = defaultSubagentAnnounceDeliveryDeps; -async function resolveQueueEmbeddedPiMessageOutcome( +async function resolveQueueEmbeddedAgentMessageOutcome( sessionId: string, text: string, - options?: EmbeddedPiQueueMessageOptions, -): Promise { - return await subagentAnnounceDeliveryDeps.queueEmbeddedPiMessageWithOutcome( + options?: EmbeddedAgentQueueMessageOptions, +): Promise { + return await subagentAnnounceDeliveryDeps.queueEmbeddedAgentMessageWithOutcome( sessionId, text, options, @@ -132,9 +132,9 @@ async function runAnnounceAgentCall(params: { function formatQueueWakeFailureError( fallback: string, - outcome: EmbeddedPiQueueMessageOutcome, + outcome: EmbeddedAgentQueueMessageOutcome, ): string { - const summary = formatEmbeddedPiQueueFailureSummary(outcome); + const summary = formatEmbeddedAgentQueueFailureSummary(outcome); return summary ? `${fallback}: ${summary}` : fallback; } @@ -202,7 +202,7 @@ function resolveRequesterSessionActivity(requesterSessionKey: string) { const sessionId = entry?.sessionId; return { sessionId, - isActive: Boolean(sessionId && isEmbeddedPiRunActive(sessionId)), + isActive: Boolean(sessionId && isEmbeddedAgentRunActive(sessionId)), }; } @@ -481,13 +481,13 @@ async function maybeSteerSubagentAnnounce(params: { // Subagent announcements are internal handoffs into an active requester turn. // Queue modes such as followup/collect apply to user prompts, not this path. - const queueOptions: EmbeddedPiQueueMessageOptions = { + const queueOptions: EmbeddedAgentQueueMessageOptions = { deliveryTimeoutMs: params.deliveryTimeoutMs, steeringMode: "all", ...(queueSettings.debounceMs !== undefined ? { debounceMs: queueSettings.debounceMs } : {}), waitForTranscriptCommit: true, }; - let queueOutcome = await resolveQueueEmbeddedPiMessageOutcome( + let queueOutcome = await resolveQueueEmbeddedAgentMessageOutcome( sessionId, params.steerMessage, queueOptions, @@ -495,7 +495,7 @@ async function maybeSteerSubagentAnnounce(params: { if (!queueOutcome.queued && queueOutcome.reason === "transcript_commit_wait_unsupported") { const bestEffortQueueOptions = { ...queueOptions }; delete bestEffortQueueOptions.waitForTranscriptCommit; - queueOutcome = await resolveQueueEmbeddedPiMessageOutcome( + queueOutcome = await resolveQueueEmbeddedAgentMessageOutcome( sessionId, params.steerMessage, bestEffortQueueOptions, @@ -1001,7 +1001,7 @@ async function sendSubagentAnnounceDirectly(params: { requesterActivity.sessionId && requesterActivity.isActive ) { - const wakeOptions: EmbeddedPiQueueMessageOptions = { + const wakeOptions: EmbeddedAgentQueueMessageOptions = { deliveryTimeoutMs: announceTimeoutMs, steeringMode: "all", ...(completionSourceReplyDeliveryMode @@ -1012,7 +1012,7 @@ async function sendSubagentAnnounceDirectly(params: { : {}), waitForTranscriptCommit: true, }; - let wakeOutcome = await resolveQueueEmbeddedPiMessageOutcome( + let wakeOutcome = await resolveQueueEmbeddedAgentMessageOutcome( requesterActivity.sessionId, params.triggerMessage, wakeOptions, @@ -1020,7 +1020,7 @@ async function sendSubagentAnnounceDirectly(params: { if (!wakeOutcome.queued && wakeOutcome.reason === "transcript_commit_wait_unsupported") { const bestEffortWakeOptions = { ...wakeOptions }; delete bestEffortWakeOptions.waitForTranscriptCommit; - wakeOutcome = await resolveQueueEmbeddedPiMessageOutcome( + wakeOutcome = await resolveQueueEmbeddedAgentMessageOutcome( requesterActivity.sessionId, params.triggerMessage, bestEffortWakeOptions, diff --git a/src/agents/subagent-announce.format.e2e.test.ts b/src/agents/subagent-announce.format.e2e.test.ts index 87141604a65..e02b4c4264a 100644 --- a/src/agents/subagent-announce.format.e2e.test.ts +++ b/src/agents/subagent-announce.format.e2e.test.ts @@ -20,10 +20,11 @@ import { buildAnnounceIdFromChildRun, buildAnnounceIdempotencyKey, } from "./announce-idempotency.js"; -import * as piEmbedded from "./pi-embedded-runner/runs.js"; +import * as embeddedRuns from "./embedded-agent-runner/runs.js"; import { testing as subagentAnnounceDeliveryTesting } from "./subagent-announce-delivery.js"; import { runSubagentAnnounceDispatch } from "./subagent-announce-dispatch.js"; import { testing as subagentAnnounceOutputTesting } from "./subagent-announce-output.js"; +import * as agentStep from "./tools/agent-step.js"; type AgentCallRequest = { method?: string; @@ -126,38 +127,39 @@ const resolveStorePathSpy = vi.spyOn(configSessions, "resolveStorePath"); const resolveMainSessionKeySpy = vi.spyOn(configSessions, "resolveMainSessionKey"); const callGatewaySpy = vi.spyOn(gatewayCall, "callGateway"); const getGlobalHookRunnerSpy = vi.spyOn(hookRunnerGlobal, "getGlobalHookRunner"); -const isEmbeddedPiRunActiveSpy = vi.spyOn(piEmbedded, "isEmbeddedPiRunActive"); -const isEmbeddedPiRunStreamingSpy = vi.spyOn(piEmbedded, "isEmbeddedPiRunStreaming"); -const queueEmbeddedPiMessageWithOutcomeSpy = vi.spyOn( - piEmbedded, - "queueEmbeddedPiMessageWithOutcome", +const readLatestAssistantReplySpy = vi.spyOn(agentStep, "readLatestAssistantReply"); +const isEmbeddedAgentRunActiveSpy = vi.spyOn(embeddedRuns, "isEmbeddedAgentRunActive"); +const isEmbeddedAgentRunStreamingSpy = vi.spyOn(embeddedRuns, "isEmbeddedAgentRunStreaming"); +const queueEmbeddedAgentMessageWithOutcomeSpy = vi.spyOn( + embeddedRuns, + "queueEmbeddedAgentMessageWithOutcome", ); -const waitForEmbeddedPiRunEndSpy = vi.spyOn(piEmbedded, "waitForEmbeddedPiRunEnd"); +const waitForEmbeddedAgentRunEndSpy = vi.spyOn(embeddedRuns, "waitForEmbeddedAgentRunEnd"); const readLatestAssistantReplyMock = vi.fn( async (_sessionKey?: string): Promise => "raw subagent reply", ); -const embeddedPiRunActiveMock = vi.fn( +const embeddedAgentRunActiveMock = vi.fn( (_sessionId: string) => false, ); -const embeddedPiRunStreamingMock = vi.fn( +const embeddedAgentRunStreamingMock = vi.fn( (_sessionId: string) => false, ); -const queueEmbeddedPiMessageWithOutcomeMock = vi.fn< - typeof piEmbedded.queueEmbeddedPiMessageWithOutcome +const queueEmbeddedAgentMessageWithOutcomeMock = vi.fn< + typeof embeddedRuns.queueEmbeddedAgentMessageWithOutcome >((sessionId: string) => ({ queued: false, sessionId, reason: "not_streaming", gatewayHealth: "live", })); -const waitForEmbeddedPiRunEndMock = vi.fn( +const waitForEmbeddedAgentRunEndMock = vi.fn( async (_sessionId: string, _timeoutMs?: number) => true, ); const embeddedRunMock = { - isEmbeddedPiRunActive: embeddedPiRunActiveMock, - isEmbeddedPiRunStreaming: embeddedPiRunStreamingMock, - queueEmbeddedPiMessageWithOutcome: queueEmbeddedPiMessageWithOutcomeMock, - waitForEmbeddedPiRunEnd: waitForEmbeddedPiRunEndMock, + isEmbeddedAgentRunActive: embeddedAgentRunActiveMock, + isEmbeddedAgentRunStreaming: embeddedAgentRunStreamingMock, + queueEmbeddedAgentMessageWithOutcome: queueEmbeddedAgentMessageWithOutcomeMock, + waitForEmbeddedAgentRunEnd: waitForEmbeddedAgentRunEndMock, }; const { subagentRegistryMock } = vi.hoisted(() => ({ subagentRegistryMock: { @@ -385,11 +387,11 @@ describe("subagent announce formatting", () => { const sessionId = entry?.sessionId; return { sessionId, - isActive: Boolean(sessionId && embeddedRunMock.isEmbeddedPiRunActive(sessionId)), + isActive: Boolean(sessionId && embeddedRunMock.isEmbeddedAgentRunActive(sessionId)), }; }, - queueEmbeddedPiMessageWithOutcome: (sessionId, text, options) => - embeddedRunMock.queueEmbeddedPiMessageWithOutcome(sessionId, text, options), + queueEmbeddedAgentMessageWithOutcome: (sessionId, text, options) => + embeddedRunMock.queueEmbeddedAgentMessageWithOutcome(sessionId, text, options), }); subagentAnnounceTesting.setDepsForTest({ callGateway: async >( @@ -415,26 +417,29 @@ describe("subagent announce formatting", () => { .mockImplementation( () => hookRunnerMock as unknown as ReturnType, ); - isEmbeddedPiRunActiveSpy + readLatestAssistantReplySpy .mockReset() - .mockImplementation((sessionId) => embeddedRunMock.isEmbeddedPiRunActive(sessionId)); - isEmbeddedPiRunStreamingSpy + .mockImplementation(async (params) => await readLatestAssistantReplyMock(params?.sessionKey)); + isEmbeddedAgentRunActiveSpy .mockReset() - .mockImplementation((sessionId) => embeddedRunMock.isEmbeddedPiRunStreaming(sessionId)); - queueEmbeddedPiMessageWithOutcomeSpy + .mockImplementation((sessionId) => embeddedRunMock.isEmbeddedAgentRunActive(sessionId)); + isEmbeddedAgentRunStreamingSpy + .mockReset() + .mockImplementation((sessionId) => embeddedRunMock.isEmbeddedAgentRunStreaming(sessionId)); + queueEmbeddedAgentMessageWithOutcomeSpy .mockReset() .mockImplementation((sessionId, text, options) => - embeddedRunMock.queueEmbeddedPiMessageWithOutcome(sessionId, text, options), + embeddedRunMock.queueEmbeddedAgentMessageWithOutcome(sessionId, text, options), ); - waitForEmbeddedPiRunEndSpy + waitForEmbeddedAgentRunEndSpy .mockReset() .mockImplementation( async (sessionId, timeoutMs) => - await embeddedRunMock.waitForEmbeddedPiRunEnd(sessionId, timeoutMs), + await embeddedRunMock.waitForEmbeddedAgentRunEnd(sessionId, timeoutMs), ); - embeddedRunMock.isEmbeddedPiRunActive.mockClear().mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockClear().mockReturnValue(false); - embeddedRunMock.queueEmbeddedPiMessageWithOutcome + embeddedRunMock.isEmbeddedAgentRunActive.mockClear().mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockClear().mockReturnValue(false); + embeddedRunMock.queueEmbeddedAgentMessageWithOutcome .mockClear() .mockImplementation((sessionId) => ({ queued: false, @@ -442,7 +447,7 @@ describe("subagent announce formatting", () => { reason: "not_streaming", gatewayHealth: "live", })); - embeddedRunMock.waitForEmbeddedPiRunEnd.mockClear().mockResolvedValue(true); + embeddedRunMock.waitForEmbeddedAgentRunEnd.mockClear().mockResolvedValue(true); subagentRegistryMock.isSubagentSessionRunActive.mockClear().mockReturnValue(true); subagentRegistryMock.shouldIgnorePostCompletionAnnounceForSession .mockClear() @@ -1801,8 +1806,8 @@ describe("subagent announce formatting", () => { }); it("keeps direct announce idempotency unique for same-ms distinct child runs", async () => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); sessionStore = { "agent:main:main": { sessionId: "session-followup", @@ -1858,8 +1863,8 @@ describe("subagent announce formatting", () => { }); it("falls back to steering when an active completion wake cannot be injected", async () => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); sessionStore = { "agent:main:main": { sessionId: "session-collect", @@ -1886,8 +1891,8 @@ describe("subagent announce formatting", () => { }); it("falls back to internal requester-session injection when completion route is missing", async () => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); sessionStore = { "agent:main:main": { sessionId: "requester-session-no-route", @@ -1951,8 +1956,8 @@ describe("subagent announce formatting", () => { }); it("returns failure for completion-mode when direct delivery fails and steering fallback is unavailable", async () => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); sessionStore = { "agent:main:main": { sessionId: "session-direct-only", @@ -2075,8 +2080,8 @@ describe("subagent announce formatting", () => { }); it("keeps announce delivery inside requester subagent session", async () => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); sessionStore = { "agent:main:subagent:orchestrator": { sessionId: "session-orchestrator", @@ -2132,8 +2137,8 @@ describe("subagent announce formatting", () => { }); it("preserves account routing for separate collect-mode announcements", async () => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); sessionStore = { "agent:main:main": { sessionId: "session-acc-split", @@ -2187,8 +2192,8 @@ describe("subagent announce formatting", () => { expectedAccountId: "acct-987", }, ] as const)("direct announce: $testName", async (testCase) => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); const didAnnounce = await runSubagentAnnounceFlow({ childSessionKey: "agent:main:subagent:test", @@ -2210,8 +2215,8 @@ describe("subagent announce formatting", () => { }); it("keeps direct announce delivery enabled for extension channels", async () => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); const didAnnounce = await runSubagentAnnounceFlow({ childSessionKey: "agent:main:subagent:test", @@ -2237,8 +2242,8 @@ describe("subagent announce formatting", () => { }); it("injects direct announce into requester subagent session as a user-turn agent call", async () => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); const didAnnounce = await runSubagentAnnounceFlow({ childSessionKey: "agent:main:subagent:worker", @@ -2260,8 +2265,8 @@ describe("subagent announce formatting", () => { }); it("keeps completion-mode announce internal for nested requester subagent sessions", async () => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); const didAnnounce = await runSubagentAnnounceFlow({ childSessionKey: "agent:main:subagent:orchestrator:subagent:worker", @@ -2288,8 +2293,8 @@ describe("subagent announce formatting", () => { }); it("retries reading subagent output when early lifecycle completion had no text", async () => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValueOnce(true).mockReturnValue(false); - embeddedRunMock.waitForEmbeddedPiRunEnd.mockResolvedValue(true); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValueOnce(true).mockReturnValue(false); + embeddedRunMock.waitForEmbeddedAgentRunEnd.mockResolvedValue(true); readLatestAssistantReplyMock .mockResolvedValueOnce(undefined) .mockResolvedValueOnce("Read #12 complete."); @@ -2316,7 +2321,10 @@ describe("subagent announce formatting", () => { outcome: { status: "ok" }, }); - expect(embeddedRunMock.waitForEmbeddedPiRunEnd).toHaveBeenCalledWith("child-session-1", 1000); + expect(embeddedRunMock.waitForEmbeddedAgentRunEnd).toHaveBeenCalledWith( + "child-session-1", + 1000, + ); const call = getAgentCall() as { params?: { message?: string } }; expect(call?.params?.message).toContain("Read #12 complete."); expect(call?.params?.message).not.toContain("(no output)"); @@ -2988,8 +2996,8 @@ describe("subagent announce formatting", () => { for (const testCase of cases) { agentSpy.mockClear(); sendSpy.mockClear(); - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(true); - embeddedRunMock.waitForEmbeddedPiRunEnd.mockResolvedValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(true); + embeddedRunMock.waitForEmbeddedAgentRunEnd.mockResolvedValue(false); sessionStore = { "agent:main:subagent:test": { sessionId: "child-session-active", @@ -3013,8 +3021,8 @@ describe("subagent announce formatting", () => { }); it("prefers requesterOrigin channel over stale session lastChannel in direct announce", async () => { - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); // Session store has stale whatsapp channel, but the requesterOrigin says imessage. sessionStore = { "agent:main:main": { @@ -3111,8 +3119,8 @@ describe("subagent announce formatting", () => { for (const testCase of cases) { agentSpy.mockClear(); - embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false); - embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunActive.mockReturnValue(false); + embeddedRunMock.isEmbeddedAgentRunStreaming.mockReturnValue(false); subagentRegistryMock.isSubagentSessionRunActive.mockReturnValue(false); sessionStore = testCase.sessionStoreFixture as SessionStoreFixture; subagentRegistryMock.resolveRequesterForChildSession.mockReturnValue({ diff --git a/src/agents/subagent-announce.live.test.ts b/src/agents/subagent-announce.live.test.ts index 071367f502f..baf1444e522 100644 --- a/src/agents/subagent-announce.live.test.ts +++ b/src/agents/subagent-announce.live.test.ts @@ -83,7 +83,7 @@ function liveSubagentConfig( if (providerConfig.provider === "google") { providers.google = { api: "google-generative-ai" as const, - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, baseUrl: "https://generativelanguage.googleapis.com/v1beta", apiKey: { source: "env" as const, @@ -96,7 +96,7 @@ function liveSubagentConfig( id: modelId, name: modelId, api: "google-generative-ai" as const, - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, input: ["text" as const], reasoning: true, contextWindow: 1_048_576, @@ -108,7 +108,7 @@ function liveSubagentConfig( } else { providers.openai = { api: "openai-responses" as const, - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, apiKey: { source: "env" as const, provider: "default" as const, @@ -121,7 +121,7 @@ function liveSubagentConfig( id: modelId, name: modelId, api: "openai-responses" as const, - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, input: ["text" as const], reasoning: true, contextWindow: 1_047_576, @@ -148,7 +148,7 @@ function liveSubagentConfig( defaults: { workspace, model: { primary: modelKey }, - models: { [modelKey]: { agentRuntime: { id: "pi" }, params: { maxTokens: 1024 } } }, + models: { [modelKey]: { agentRuntime: { id: "openclaw" }, params: { maxTokens: 1024 } } }, sandbox: { mode: "off" }, subagents: { allowAgents: ["*"], diff --git a/src/agents/subagent-announce.runtime.ts b/src/agents/subagent-announce.runtime.ts index 9bc2beb4ba0..5d6c81ef653 100644 --- a/src/agents/subagent-announce.runtime.ts +++ b/src/agents/subagent-announce.runtime.ts @@ -8,4 +8,7 @@ export { export { callGateway } from "../gateway/call.js"; export { readSessionMessagesAsync } from "../gateway/session-utils.fs.js"; export { dispatchGatewayMethodInProcess } from "../gateway/server-plugins.js"; -export { isEmbeddedPiRunActive, waitForEmbeddedPiRunEnd } from "./pi-embedded-runner/runs.js"; +export { + isEmbeddedAgentRunActive, + waitForEmbeddedAgentRunEnd, +} from "./embedded-agent-runner/runs.js"; diff --git a/src/agents/subagent-announce.test-support.ts b/src/agents/subagent-announce.test-support.ts index 52213191073..bc18605957b 100644 --- a/src/agents/subagent-announce.test-support.ts +++ b/src/agents/subagent-announce.test-support.ts @@ -1,8 +1,8 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { callGateway } from "../gateway/call.js"; import type { dispatchGatewayMethodInProcess } from "../gateway/server-plugins.js"; -import type { EmbeddedPiQueueMessageOptions } from "./pi-embedded-runner/run-state.js"; -import type { EmbeddedPiQueueMessageOutcome } from "./pi-embedded-runner/runs.js"; +import type { EmbeddedAgentQueueMessageOptions } from "./embedded-agent-runner/run-state.js"; +import type { EmbeddedAgentQueueMessageOutcome } from "./embedded-agent-runner/runs.js"; type DeliveryRuntimeMockOptions = { callGateway: (request: unknown) => Promise; @@ -11,12 +11,12 @@ type DeliveryRuntimeMockOptions = { resolveAgentIdFromSessionKey: (sessionKey: string) => string; resolveMainSessionKey: (cfg: unknown) => string; resolveStorePath: (store: unknown, options: unknown) => string; - isEmbeddedPiRunActive: (sessionId: string) => boolean; - queueEmbeddedPiMessageWithOutcome: ( + isEmbeddedAgentRunActive: (sessionId: string) => boolean; + queueEmbeddedAgentMessageWithOutcome: ( sessionId: string, text: string, - options?: EmbeddedPiQueueMessageOptions, - ) => EmbeddedPiQueueMessageOutcome; + options?: EmbeddedAgentQueueMessageOptions, + ) => EmbeddedAgentQueueMessageOutcome; hasHooks?: () => boolean; }; @@ -71,9 +71,9 @@ export function createSubagentAnnounceDeliveryRuntimeMock(options: DeliveryRunti resolveAgentIdFromSessionKey: options.resolveAgentIdFromSessionKey, resolveMainSessionKey: options.resolveMainSessionKey, resolveStorePath: options.resolveStorePath, - isEmbeddedPiRunActive: options.isEmbeddedPiRunActive, - queueEmbeddedPiMessageWithOutcome: options.queueEmbeddedPiMessageWithOutcome, - formatEmbeddedPiQueueFailureSummary: (outcome: { reason?: string; sessionId?: string }) => + isEmbeddedAgentRunActive: options.isEmbeddedAgentRunActive, + queueEmbeddedAgentMessageWithOutcome: options.queueEmbeddedAgentMessageWithOutcome, + formatEmbeddedAgentQueueFailureSummary: (outcome: { reason?: string; sessionId?: string }) => outcome.reason && outcome.sessionId ? `queue_message_failed reason=${outcome.reason} sessionId=${outcome.sessionId} gatewayHealth=live` : undefined, diff --git a/src/agents/subagent-announce.test.ts b/src/agents/subagent-announce.test.ts index 75e8285883d..932f4d5070f 100644 --- a/src/agents/subagent-announce.test.ts +++ b/src/agents/subagent-announce.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import type { EmbeddedPiQueueMessageOutcome } from "./pi-embedded-runner/runs.js"; +import type { EmbeddedAgentQueueMessageOutcome } from "./embedded-agent-runner/runs.js"; import { createSubagentAnnounceDeliveryRuntimeMock } from "./subagent-announce.test-support.js"; type AgentCallRequest = { method?: string; params?: Record }; @@ -20,16 +20,18 @@ const resolveAgentIdFromSessionKeyMock = vi.fn((sessionKey: string) => { const resolveStorePathMock = vi.fn((_store: unknown, _options: unknown) => "/tmp/sessions.json"); const resolveMainSessionKeyMock = vi.fn((_cfg: unknown) => "agent:main:main"); const readLatestAssistantReplyMock = vi.fn(async (_params?: unknown) => "raw subagent reply"); -const isEmbeddedPiRunActiveMock = vi.fn((_sessionId: string) => false); -const queueEmbeddedPiMessageWithOutcomeMock = vi.fn( - (sessionId: string, _text: string, _options?: unknown): EmbeddedPiQueueMessageOutcome => ({ +const isEmbeddedAgentRunActiveMock = vi.fn((_sessionId: string) => false); +const queueEmbeddedAgentMessageWithOutcomeMock = vi.fn( + (sessionId: string, _text: string, _options?: unknown): EmbeddedAgentQueueMessageOutcome => ({ queued: false, sessionId, reason: "not_streaming" as const, gatewayHealth: "live" as const, }), ); -const waitForEmbeddedPiRunEndMock = vi.fn(async (_sessionId: string, _timeoutMs?: number) => true); +const waitForEmbeddedAgentRunEndMock = vi.fn( + async (_sessionId: string, _timeoutMs?: number) => true, +); let mockConfig: ReturnType<(typeof import("../config/config.js"))["getRuntimeConfig"]> = { session: { mainKey: "main", @@ -57,7 +59,7 @@ vi.mock("./subagent-announce.runtime.js", () => ({ params: Record, options?: { timeoutMs?: number }, ) => callGatewayMock({ method, params, timeoutMs: options?.timeoutMs }), - isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId), + isEmbeddedAgentRunActive: (sessionId: string) => isEmbeddedAgentRunActiveMock(sessionId), getRuntimeConfig: () => mockConfig, loadSessionStore: (storePath: string) => loadSessionStoreMock(storePath), readSessionMessagesAsync: vi.fn(async () => []), @@ -67,8 +69,8 @@ vi.mock("./subagent-announce.runtime.js", () => ({ resolveAgentIdFromSessionKeyMock(sessionKey), resolveMainSessionKey: (cfg: unknown) => resolveMainSessionKeyMock(cfg), resolveStorePath: (store: unknown, options: unknown) => resolveStorePathMock(store, options), - waitForEmbeddedPiRunEnd: (sessionId: string, timeoutMs?: number) => - waitForEmbeddedPiRunEndMock(sessionId, timeoutMs), + waitForEmbeddedAgentRunEnd: (sessionId: string, timeoutMs?: number) => + waitForEmbeddedAgentRunEndMock(sessionId, timeoutMs), })); vi.mock("./tools/agent-step.js", () => ({ @@ -84,9 +86,9 @@ vi.mock("./subagent-announce-delivery.runtime.js", () => resolveAgentIdFromSessionKeyMock(sessionKey), resolveMainSessionKey: (cfg: unknown) => resolveMainSessionKeyMock(cfg), resolveStorePath: (store: unknown, options: unknown) => resolveStorePathMock(store, options), - isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId), - queueEmbeddedPiMessageWithOutcome: (sessionId: string, text: string, options?: unknown) => - queueEmbeddedPiMessageWithOutcomeMock(sessionId, text, options), + isEmbeddedAgentRunActive: (sessionId: string) => isEmbeddedAgentRunActiveMock(sessionId), + queueEmbeddedAgentMessageWithOutcome: (sessionId: string, text: string, options?: unknown) => + queueEmbeddedAgentMessageWithOutcomeMock(sessionId, text, options), }), ); @@ -117,8 +119,8 @@ vi.mock("./subagent-announce-delivery.js", () => ({ params.requesterSessionOrigin?.provider ?? params.requesterSessionOrigin?.channel; - if (sessionId && queueChannel === "discord" && isEmbeddedPiRunActiveMock(sessionId)) { - queueEmbeddedPiMessageWithOutcomeMock( + if (sessionId && queueChannel === "discord" && isEmbeddedAgentRunActiveMock(sessionId)) { + queueEmbeddedAgentMessageWithOutcomeMock( sessionId, `[Internal task completion event]\n${params.triggerMessage}`, { steeringMode: "all" }, @@ -198,7 +200,7 @@ import { applySubagentWaitOutcome } from "./subagent-announce-output.js"; import { runSubagentAnnounceFlow } from "./subagent-announce.js"; function requireQueuedMessageCall() { - const call = queueEmbeddedPiMessageWithOutcomeMock.mock.calls[0]; + const call = queueEmbeddedAgentMessageWithOutcomeMock.mock.calls[0]; if (!call) { throw new Error("expected queued message call"); } @@ -267,14 +269,16 @@ describe("subagent announce seam flow", () => { resolveStorePathMock.mockReset().mockImplementation(() => "/tmp/sessions.json"); resolveMainSessionKeyMock.mockReset().mockImplementation(() => "agent:main:main"); readLatestAssistantReplyMock.mockReset().mockResolvedValue("raw subagent reply"); - isEmbeddedPiRunActiveMock.mockReset().mockReturnValue(false); - queueEmbeddedPiMessageWithOutcomeMock.mockReset().mockImplementation((sessionId: string) => ({ - queued: false, - sessionId, - reason: "not_streaming", - gatewayHealth: "live", - })); - waitForEmbeddedPiRunEndMock.mockReset().mockResolvedValue(true); + isEmbeddedAgentRunActiveMock.mockReset().mockReturnValue(false); + queueEmbeddedAgentMessageWithOutcomeMock + .mockReset() + .mockImplementation((sessionId: string) => ({ + queued: false, + sessionId, + reason: "not_streaming", + gatewayHealth: "live", + })); + waitForEmbeddedAgentRunEndMock.mockReset().mockResolvedValue(true); mockConfig = { session: { mainKey: "main", @@ -381,8 +385,8 @@ describe("subagent announce seam flow", () => { origin: { provider: "discord" }, }, })); - isEmbeddedPiRunActiveMock.mockReturnValue(true); - queueEmbeddedPiMessageWithOutcomeMock.mockImplementation((sessionId: string) => ({ + isEmbeddedAgentRunActiveMock.mockReturnValue(true); + queueEmbeddedAgentMessageWithOutcomeMock.mockImplementation((sessionId: string) => ({ queued: true, sessionId, target: "embedded_run", diff --git a/src/agents/subagent-announce.timeout.test.ts b/src/agents/subagent-announce.timeout.test.ts index bfcff5ee7b2..50a18ee7e3c 100644 --- a/src/agents/subagent-announce.timeout.test.ts +++ b/src/agents/subagent-announce.timeout.test.ts @@ -26,8 +26,10 @@ let requesterDepthResolver: (sessionKey?: string) => number = () => 0; let subagentSessionRunActive = true; let shouldIgnorePostCompletion = false; let pendingDescendantRuns = 0; -const isEmbeddedPiRunActiveMock = vi.fn((_sessionId: string) => false); -const waitForEmbeddedPiRunEndMock = vi.fn(async (_sessionId: string, _timeoutMs?: number) => true); +const isEmbeddedAgentRunActiveMock = vi.fn((_sessionId: string) => false); +const waitForEmbeddedAgentRunEndMock = vi.fn( + async (_sessionId: string, _timeoutMs?: number) => true, +); let fallbackRequesterResolution: { requesterSessionKey: string; requesterOrigin?: { channel?: string; to?: string; accountId?: string }; @@ -87,8 +89,8 @@ vi.mock("./subagent-announce-delivery.runtime.js", () => resolveAgentIdFromSessionKey: () => "main", resolveMainSessionKey: () => "agent:main:main", resolveStorePath: () => "/tmp/sessions-main.json", - isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId), - queueEmbeddedPiMessageWithOutcome: (sessionId: string) => ({ + isEmbeddedAgentRunActive: (sessionId: string) => isEmbeddedAgentRunActiveMock(sessionId), + queueEmbeddedAgentMessageWithOutcome: (sessionId: string) => ({ queued: false, sessionId, reason: "not_streaming", @@ -196,9 +198,9 @@ vi.mock("./subagent-announce.runtime.js", () => ({ resolveAgentIdFromSessionKey: () => "main", resolveStorePath: () => "/tmp/sessions-main.json", resolveMainSessionKey: () => "agent:main:main", - isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId), - waitForEmbeddedPiRunEnd: (sessionId: string, timeoutMs?: number) => - waitForEmbeddedPiRunEndMock(sessionId, timeoutMs), + isEmbeddedAgentRunActive: (sessionId: string) => isEmbeddedAgentRunActiveMock(sessionId), + waitForEmbeddedAgentRunEnd: (sessionId: string, timeoutMs?: number) => + waitForEmbeddedAgentRunEndMock(sessionId, timeoutMs), })); vi.mock("./subagent-announce.registry.runtime.js", () => ({ countActiveDescendantRuns: () => 0, @@ -293,8 +295,8 @@ describe("subagent announce timeout config", () => { subagentSessionRunActive = true; shouldIgnorePostCompletion = false; pendingDescendantRuns = 0; - isEmbeddedPiRunActiveMock.mockReset().mockReturnValue(false); - waitForEmbeddedPiRunEndMock.mockReset().mockResolvedValue(true); + isEmbeddedAgentRunActiveMock.mockReset().mockReturnValue(false); + waitForEmbeddedAgentRunEndMock.mockReset().mockResolvedValue(true); fallbackRequesterResolution = null; }); diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index c2aca1beed3..106e714f9b7 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -40,9 +40,9 @@ import { import { callGateway, dispatchGatewayMethodInProcess, - isEmbeddedPiRunActive, + isEmbeddedAgentRunActive, getRuntimeConfig, - waitForEmbeddedPiRunEnd, + waitForEmbeddedAgentRunEnd, } from "./subagent-announce.runtime.js"; import { getSubagentDepthFromSessionStore } from "./subagent-depth.js"; import { deleteSubagentSessionForCleanup } from "./subagent-session-cleanup.js"; @@ -269,9 +269,9 @@ export async function runSubagentAnnounceFlow(params: { const settleTimeoutMs = Math.min(Math.max(params.timeoutMs, 1), 120_000); let reply = params.roundOneReply; let outcome: SubagentRunOutcome | undefined = params.outcome; - if (childSessionId && isEmbeddedPiRunActive(childSessionId)) { - const settled = await waitForEmbeddedPiRunEnd(childSessionId, settleTimeoutMs); - if (!settled && isEmbeddedPiRunActive(childSessionId)) { + if (childSessionId && isEmbeddedAgentRunActive(childSessionId)) { + const settled = await waitForEmbeddedAgentRunEnd(childSessionId, settleTimeoutMs); + if (!settled && isEmbeddedAgentRunActive(childSessionId)) { shouldDeleteChildSession = false; return false; } diff --git a/src/agents/subagent-control.runtime.ts b/src/agents/subagent-control.runtime.ts index bffb3c46024..8563792dc2b 100644 --- a/src/agents/subagent-control.runtime.ts +++ b/src/agents/subagent-control.runtime.ts @@ -1,2 +1,2 @@ export { clearSessionQueues } from "../auto-reply/reply/queue.js"; -export { abortEmbeddedPiRun } from "./pi-embedded-runner/runs.js"; +export { abortEmbeddedAgentRun } from "./embedded-agent-runner/runs.js"; diff --git a/src/agents/subagent-control.test.ts b/src/agents/subagent-control.test.ts index 57c108e8fc9..51197e6759e 100644 --- a/src/agents/subagent-control.test.ts +++ b/src/agents/subagent-control.test.ts @@ -112,7 +112,7 @@ function setSubagentControlDepsForTest( overrides: Parameters[0] = {}, ) { testing.setDepsForTest({ - abortEmbeddedPiRun: () => false, + abortEmbeddedAgentRun: () => false, clearSessionQueues: () => ({ followupCleared: 0, laneCleared: 0, keys: [] }), updateSessionStore: async ( storePath: string, diff --git a/src/agents/subagent-control.ts b/src/agents/subagent-control.ts index 5f8b6137300..4f12121a9b3 100644 --- a/src/agents/subagent-control.ts +++ b/src/agents/subagent-control.ts @@ -50,7 +50,7 @@ const steerRateLimit = new Map(); type GatewayCaller = typeof callGateway; type UpdateSessionStore = typeof updateSessionStore; -type AbortEmbeddedPiRun = (sessionId: string) => boolean; +type AbortEmbeddedAgentRun = (sessionId: string) => boolean; type ClearSessionQueues = (keys: Array) => ClearSessionQueueResult; const defaultSubagentControlDeps = { @@ -61,7 +61,7 @@ const defaultSubagentControlDeps = { let subagentControlDeps: { callGateway: GatewayCaller; updateSessionStore: UpdateSessionStore; - abortEmbeddedPiRun?: AbortEmbeddedPiRun; + abortEmbeddedAgentRun?: AbortEmbeddedAgentRun; clearSessionQueues?: ClearSessionQueues; } = defaultSubagentControlDeps; @@ -74,18 +74,19 @@ function loadSubagentControlRuntime() { } async function resolveSubagentControlRuntime(): Promise<{ - abortEmbeddedPiRun: AbortEmbeddedPiRun; + abortEmbeddedAgentRun: AbortEmbeddedAgentRun; clearSessionQueues: ClearSessionQueues; }> { - if (subagentControlDeps.abortEmbeddedPiRun && subagentControlDeps.clearSessionQueues) { + if (subagentControlDeps.abortEmbeddedAgentRun && subagentControlDeps.clearSessionQueues) { return { - abortEmbeddedPiRun: subagentControlDeps.abortEmbeddedPiRun, + abortEmbeddedAgentRun: subagentControlDeps.abortEmbeddedAgentRun, clearSessionQueues: subagentControlDeps.clearSessionQueues, }; } const runtime = await loadSubagentControlRuntime(); return { - abortEmbeddedPiRun: subagentControlDeps.abortEmbeddedPiRun ?? runtime.abortEmbeddedPiRun, + abortEmbeddedAgentRun: + subagentControlDeps.abortEmbeddedAgentRun ?? runtime.abortEmbeddedAgentRun, clearSessionQueues: subagentControlDeps.clearSessionQueues ?? runtime.clearSessionQueues, }; } @@ -169,7 +170,7 @@ async function killSubagentRun(params: { }); const sessionId = resolved.entry?.sessionId; const runtime = await resolveSubagentControlRuntime(); - const aborted = sessionId ? runtime.abortEmbeddedPiRun(sessionId) : false; + const aborted = sessionId ? runtime.abortEmbeddedAgentRun(sessionId) : false; const cleared = runtime.clearSessionQueues([childSessionKey, sessionId]); if (cleared.followupCleared > 0 || cleared.laneCleared > 0) { logVerbose( @@ -530,7 +531,7 @@ export async function steerControlledSubagentRun(params: { if (sessionId) { const runtime = await resolveSubagentControlRuntime(); - runtime.abortEmbeddedPiRun(sessionId); + runtime.abortEmbeddedAgentRun(sessionId); } const runtime = await resolveSubagentControlRuntime(); const cleared = runtime.clearSessionQueues([params.entry.childSessionKey, sessionId]); @@ -733,7 +734,7 @@ export const testing = { overrides?: Partial<{ callGateway: GatewayCaller; updateSessionStore: UpdateSessionStore; - abortEmbeddedPiRun: AbortEmbeddedPiRun; + abortEmbeddedAgentRun: AbortEmbeddedAgentRun; clearSessionQueues: ClearSessionQueues; }>, ) { diff --git a/src/agents/subagent-registry-lifecycle.test.ts b/src/agents/subagent-registry-lifecycle.test.ts index 37fe4969c56..721ab577652 100644 --- a/src/agents/subagent-registry-lifecycle.test.ts +++ b/src/agents/subagent-registry-lifecycle.test.ts @@ -62,7 +62,7 @@ vi.mock("../browser-lifecycle-cleanup.js", () => ({ browserLifecycleCleanupMocks.cleanupBrowserSessionsForLifecycleEnd, })); -vi.mock("./pi-bundle-mcp-tools.js", () => ({ +vi.mock("./agent-bundle-mcp-tools.js", () => ({ retireSessionMcpRuntimeForSessionKey: bundleMcpRuntimeMocks.retireSessionMcpRuntimeForSessionKey, })); diff --git a/src/agents/subagent-registry-lifecycle.ts b/src/agents/subagent-registry-lifecycle.ts index 718711196e9..294d5434ab9 100644 --- a/src/agents/subagent-registry-lifecycle.ts +++ b/src/agents/subagent-registry-lifecycle.ts @@ -17,12 +17,12 @@ import { resolveRequiredCompletionTerminalResult, } from "../tasks/task-completion-contract.js"; import { normalizeDeliveryContext } from "../utils/delivery-context.shared.js"; +import { retireSessionMcpRuntimeForSessionKey } from "./agent-bundle-mcp-tools.js"; import { buildAnnounceIdFromChildRun, buildAnnounceIdempotencyKey, } from "./announce-idempotency.js"; import { removeInternalSessionEffectsTranscript } from "./internal-session-effects.js"; -import { retireSessionMcpRuntimeForSessionKey } from "./pi-bundle-mcp-tools.js"; import type { SubagentAnnounceDeliveryResult } from "./subagent-announce-dispatch.js"; import { type SubagentRunOutcome, withSubagentOutcomeTiming } from "./subagent-announce-output.js"; import { diff --git a/src/agents/subagent-spawn.workspace.test.ts b/src/agents/subagent-spawn.workspace.test.ts index 6eef6cf5c4f..f3c43649a3b 100644 --- a/src/agents/subagent-spawn.workspace.test.ts +++ b/src/agents/subagent-spawn.workspace.test.ts @@ -32,9 +32,9 @@ const hoisted = vi.hoisted(() => ({ let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect; let resetSubagentRegistryForTests: typeof import("./subagent-registry.js").resetSubagentRegistryForTests; -vi.mock("@earendil-works/pi-ai/oauth", async () => { - const actual = await vi.importActual( - "@earendil-works/pi-ai/oauth", +vi.mock("openclaw/plugin-sdk/llm-oauth", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/llm-oauth", ); return { ...actual, diff --git a/src/agents/system-prompt-report.ts b/src/agents/system-prompt-report.ts index d888c74c960..643153f2f8e 100644 --- a/src/agents/system-prompt-report.ts +++ b/src/agents/system-prompt-report.ts @@ -1,8 +1,7 @@ -import { createHash } from "node:crypto"; -import type { AgentTool } from "@earendil-works/pi-agent-core"; import type { SessionSystemPromptReport } from "../config/sessions/types.js"; import { buildBootstrapInjectionStats } from "./bootstrap-budget.js"; -import type { EmbeddedContextFile } from "./pi-embedded-helpers.js"; +import type { EmbeddedContextFile } from "./embedded-agent-helpers.js"; +import type { AgentTool } from "./runtime/index.js"; import type { WorkspaceBootstrapFile } from "./workspace.js"; type ToolReportEntry = SessionSystemPromptReport["tools"]["entries"][number]; @@ -10,47 +9,9 @@ type ToolReportEntry = SessionSystemPromptReport["tools"]["entries"][number]; const toolReportEntryCache = new WeakMap(); const toolSchemaStatsCache = new WeakMap< object, - Pick + Pick >(); -function sha256(value: string): string { - return createHash("sha256").update(value).digest("hex"); -} - -function normalizeForStableHash(value: unknown, seen = new WeakSet()): unknown { - if (typeof value === "bigint") { - return `${value.toString()}n`; - } - if (value && typeof value === "object") { - if (seen.has(value)) { - return "[Circular]"; - } - seen.add(value); - if (Array.isArray(value)) { - const normalized = value.map((entry) => normalizeForStableHash(entry, seen)); - seen.delete(value); - return normalized; - } - const record = value as Record; - const normalized = Object.fromEntries( - Object.keys(record) - .toSorted((left, right) => left.localeCompare(right)) - .map((key) => [key, normalizeForStableHash(record[key], seen)]), - ); - seen.delete(value); - return normalized; - } - return value; -} - -function stableJsonHash(value: unknown): string { - try { - return sha256(JSON.stringify(normalizeForStableHash(value)) ?? "null"); - } catch { - return sha256("[unserializable]"); - } -} - function extractBetween(input: string, startMarker: string, endMarker: string): string { const start = input.indexOf(startMarker); if (start === -1) { @@ -78,9 +39,9 @@ function parseSkillBlocks(skillsPrompt: string): Array<{ name: string; blockChar function buildToolSchemaStats( parameters: AgentTool["parameters"], -): Pick { +): Pick { if (!parameters || typeof parameters !== "object") { - return { schemaChars: 0, schemaHash: stableJsonHash(null), propertiesCount: null }; + return { schemaChars: 0, propertiesCount: null }; } const cached = toolSchemaStatsCache.get(parameters); if (cached) { @@ -94,7 +55,6 @@ function buildToolSchemaStats( return 0; } })(), - schemaHash: stableJsonHash(parameters), propertiesCount: (() => { const schema = parameters as Record; const props = typeof schema.properties === "object" ? schema.properties : null; @@ -118,7 +78,7 @@ function buildToolsEntries(tools: AgentTool[]): SessionSystemPromptReport["tools const summary = tool.description?.trim() || tool.label?.trim() || ""; const summaryChars = summary.length; const schemaStats = buildToolSchemaStats(tool.parameters); - const entry = { name, summaryChars, summaryHash: sha256(summary), ...schemaStats }; + const entry = { name, summaryChars, ...schemaStats }; toolReportEntryCache.set(tool, entry); return entry; }); @@ -169,7 +129,6 @@ export function buildSystemPromptReport(params: { chars: systemPromptChars, projectContextChars, nonProjectContextChars: Math.max(0, systemPromptChars - projectContextChars), - hash: sha256(params.systemPrompt), }, ...(params.currentTurn ? { currentTurn: params.currentTurn } : {}), injectedWorkspaceFiles: buildBootstrapInjectionStats({ @@ -178,7 +137,6 @@ export function buildSystemPromptReport(params: { }), skills: { promptChars: params.skillsPrompt.length, - hash: sha256(params.skillsPrompt), entries: skillsEntries, }, tools: { diff --git a/src/agents/system-prompt.test.ts b/src/agents/system-prompt.test.ts index d44a7b8fe15..72630d9b578 100644 --- a/src/agents/system-prompt.test.ts +++ b/src/agents/system-prompt.test.ts @@ -18,8 +18,8 @@ describe("buildAgentSystemPrompt", () => { it("resolves helper session keys to scoped prompt surfaces", () => { expect(resolveAgentPromptSurfaceForSessionKey("agent:main:subagent:child")).toBe("subagent"); expect(resolveAgentPromptSurfaceForSessionKey("agent:codex:acp:child")).toBe("acp_backend"); - expect(resolveAgentPromptSurfaceForSessionKey("agent:main")).toBe("pi_main"); - expect(resolveAgentPromptSurfaceForSessionKey(undefined)).toBe("pi_main"); + expect(resolveAgentPromptSurfaceForSessionKey("agent:main")).toBe("openclaw_main"); + expect(resolveAgentPromptSurfaceForSessionKey(undefined)).toBe("openclaw_main"); }); it("formats owner section for plain, hash, and missing owner lists", () => { @@ -375,13 +375,13 @@ describe("buildAgentSystemPrompt", () => { expect(prompt).not.toContain("Brave API"); }); - it("keeps the PI empty-tool fallback on the main prompt surface", () => { + it("keeps the OpenClaw empty-tool fallback on the main prompt surface", () => { const prompt = buildAgentSystemPrompt({ workspaceDir: "/tmp/openclaw", toolNames: [], }); - expect(prompt).toContain("Pi lists the standard tools above"); + expect(prompt).toContain("OpenClaw lists the standard tools above"); expect(prompt).toContain("- sessions_spawn: spawn an isolated sub-agent session"); }); diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index dab75fb3928..a3fef656551 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -27,11 +27,11 @@ import { buildLimitedBootstrapPromptLines, } from "./bootstrap-prompt.js"; import type { ResolvedTimeFormat } from "./date-time.js"; -import type { EmbeddedContextFile } from "./pi-embedded-helpers.js"; +import type { EmbeddedContextFile } from "./embedded-agent-helpers.js"; import type { EmbeddedFullAccessBlockedReason, EmbeddedSandboxInfo, -} from "./pi-embedded-runner/types.js"; +} from "./embedded-agent-runner/types.js"; import { normalizePromptCapabilityIds, normalizeStructuredPromptSection, @@ -706,7 +706,7 @@ export function buildAgentSystemPrompt(params: { subagentDelegationMode?: SubagentDelegationMode; /** Whether ACP-specific routing guidance should be included. Defaults to true. */ acpEnabled?: boolean; - /** Prompt surface controls runtime-specific fallback fragments. Defaults to PI main. */ + /** Prompt surface controls runtime-specific fallback fragments. Defaults to OpenClaw main. */ promptSurface?: AgentPromptSurfaceKind; /** Registered runtime slash/native command names such as `codex`. */ nativeCommandNames?: string[]; @@ -740,7 +740,7 @@ export function buildAgentSystemPrompt(params: { promptContribution?: ProviderSystemPromptContribution; }) { const acpEnabled = params.acpEnabled === true; - const promptSurface = params.promptSurface ?? "pi_main"; + const promptSurface = params.promptSurface ?? "openclaw_main"; const sandboxedRuntime = params.sandboxInfo?.enabled === true; const acpSpawnRuntimeEnabled = acpEnabled && !sandboxedRuntime; const coreToolSummaries: Record = { diff --git a/src/agents/test-helpers/agent-message-fixtures.ts b/src/agents/test-helpers/agent-message-fixtures.ts index 64be4a0bebd..dac2410472e 100644 --- a/src/agents/test-helpers/agent-message-fixtures.ts +++ b/src/agents/test-helpers/agent-message-fixtures.ts @@ -1,5 +1,5 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { AssistantMessage, UserMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage, UserMessage } from "openclaw/plugin-sdk/llm"; +import type { AgentMessage } from "../runtime/index.js"; import { ZERO_USAGE_FIXTURE } from "./usage-fixtures.js"; export function castAgentMessage(message: unknown): AgentMessage { diff --git a/src/agents/test-helpers/pi-coding-agent-token-mock.ts b/src/agents/test-helpers/agent-session-token-mock.ts similarity index 71% rename from src/agents/test-helpers/pi-coding-agent-token-mock.ts rename to src/agents/test-helpers/agent-session-token-mock.ts index ea978bc2a26..1d4d66dec86 100644 --- a/src/agents/test-helpers/pi-coding-agent-token-mock.ts +++ b/src/agents/test-helpers/agent-session-token-mock.ts @@ -1,6 +1,6 @@ import { vi } from "vitest"; -const piCodingAgentTokenMocks = vi.hoisted(() => { +const agentSessionTokenMocks = vi.hoisted(() => { function readText(value: unknown): string { if (typeof value === "string") { return value; @@ -24,12 +24,12 @@ const piCodingAgentTokenMocks = vi.hoisted(() => { }; }); -vi.mock("@earendil-works/pi-coding-agent", async () => { - const actual = await vi.importActual( - "@earendil-works/pi-coding-agent", +vi.mock("openclaw/plugin-sdk/agent-sessions", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/agent-sessions", ); return { ...actual, - estimateTokens: piCodingAgentTokenMocks.estimateTokens, + estimateTokens: agentSessionTokenMocks.estimateTokens, }; }); diff --git a/src/agents/test-helpers/pi-tool-stubs.ts b/src/agents/test-helpers/agent-tool-stubs.ts similarity index 75% rename from src/agents/test-helpers/pi-tool-stubs.ts rename to src/agents/test-helpers/agent-tool-stubs.ts index 746fc8830da..d3c25b29f0b 100644 --- a/src/agents/test-helpers/pi-tool-stubs.ts +++ b/src/agents/test-helpers/agent-tool-stubs.ts @@ -1,5 +1,5 @@ -import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core"; import { Type } from "typebox"; +import type { AgentTool, AgentToolResult } from "../runtime/index.js"; export function createStubTool(name: string): AgentTool { return { diff --git a/src/agents/test-helpers/pi-tools-fs-helpers.ts b/src/agents/test-helpers/agent-tools-fs-helpers.ts similarity index 100% rename from src/agents/test-helpers/pi-tools-fs-helpers.ts rename to src/agents/test-helpers/agent-tools-fs-helpers.ts diff --git a/src/agents/test-helpers/pi-tools-sandbox-context.test.ts b/src/agents/test-helpers/agent-tools-sandbox-context.test.ts similarity index 82% rename from src/agents/test-helpers/pi-tools-sandbox-context.test.ts rename to src/agents/test-helpers/agent-tools-sandbox-context.test.ts index 63e09b6f7c6..dfdbcc9e80a 100644 --- a/src/agents/test-helpers/pi-tools-sandbox-context.test.ts +++ b/src/agents/test-helpers/agent-tools-sandbox-context.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import { createPiToolsSandboxContext } from "./pi-tools-sandbox-context.js"; +import { createAgentToolsSandboxContext } from "./agent-tools-sandbox-context.js"; -describe("createPiToolsSandboxContext", () => { - it("provides stable defaults for pi-tools sandbox tests", () => { - const sandbox = createPiToolsSandboxContext({ +describe("createAgentToolsSandboxContext", () => { + it("provides stable defaults for agent-tools sandbox tests", () => { + const sandbox = createAgentToolsSandboxContext({ workspaceDir: "/tmp/sandbox", }); @@ -21,7 +21,7 @@ describe("createPiToolsSandboxContext", () => { }); it("applies provided overrides", () => { - const sandbox = createPiToolsSandboxContext({ + const sandbox = createAgentToolsSandboxContext({ workspaceDir: "/tmp/sandbox", agentWorkspaceDir: "/tmp/workspace", workspaceAccess: "ro", diff --git a/src/agents/test-helpers/pi-tools-sandbox-context.ts b/src/agents/test-helpers/agent-tools-sandbox-context.ts similarity index 90% rename from src/agents/test-helpers/pi-tools-sandbox-context.ts rename to src/agents/test-helpers/agent-tools-sandbox-context.ts index abf712c2c0b..7bb4260f4fe 100644 --- a/src/agents/test-helpers/pi-tools-sandbox-context.ts +++ b/src/agents/test-helpers/agent-tools-sandbox-context.ts @@ -1,7 +1,7 @@ import type { SandboxContext, SandboxToolPolicy, SandboxWorkspaceAccess } from "../sandbox.js"; import type { SandboxFsBridge } from "../sandbox/fs-bridge.js"; -type PiToolsSandboxContextParams = { +type AgentToolsSandboxContextParams = { workspaceDir: string; agentWorkspaceDir?: string; workspaceAccess?: SandboxWorkspaceAccess; @@ -14,7 +14,9 @@ type PiToolsSandboxContextParams = { dockerOverrides?: Partial; }; -export function createPiToolsSandboxContext(params: PiToolsSandboxContextParams): SandboxContext { +export function createAgentToolsSandboxContext( + params: AgentToolsSandboxContextParams, +): SandboxContext { const workspaceDir = params.workspaceDir; return { enabled: true, diff --git a/src/agents/test-helpers/assistant-message-fixtures.ts b/src/agents/test-helpers/assistant-message-fixtures.ts index a95624266f2..682751ab45f 100644 --- a/src/agents/test-helpers/assistant-message-fixtures.ts +++ b/src/agents/test-helpers/assistant-message-fixtures.ts @@ -1,4 +1,4 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { ZERO_USAGE_FIXTURE } from "./usage-fixtures.js"; export function makeAssistantMessageFixture( diff --git a/src/agents/test-helpers/pi-embedded-runner-e2e-fixtures.ts b/src/agents/test-helpers/embedded-agent-runner-e2e-fixtures.ts similarity index 86% rename from src/agents/test-helpers/pi-embedded-runner-e2e-fixtures.ts rename to src/agents/test-helpers/embedded-agent-runner-e2e-fixtures.ts index ffb3d57d6ec..c34c67d28c1 100644 --- a/src/agents/test-helpers/pi-embedded-runner-e2e-fixtures.ts +++ b/src/agents/test-helpers/embedded-agent-runner-e2e-fixtures.ts @@ -1,20 +1,20 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; -import { buildAttemptReplayMetadata } from "../pi-embedded-runner/run/incomplete-turn.js"; -import type { EmbeddedRunAttemptResult } from "../pi-embedded-runner/run/types.js"; +import { buildAttemptReplayMetadata } from "../embedded-agent-runner/run/incomplete-turn.js"; +import type { EmbeddedRunAttemptResult } from "../embedded-agent-runner/run/types.js"; -export type EmbeddedPiRunnerTestWorkspace = { +export type EmbeddedAgentRunnerTestWorkspace = { tempRoot: string; agentDir: string; workspaceDir: string; }; -export async function createEmbeddedPiRunnerTestWorkspace( +export async function createEmbeddedAgentRunnerTestWorkspace( prefix: string, -): Promise { +): Promise { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); const agentDir = path.join(tempRoot, "agent"); const workspaceDir = path.join(tempRoot, "workspace"); @@ -23,8 +23,8 @@ export async function createEmbeddedPiRunnerTestWorkspace( return { tempRoot, agentDir, workspaceDir }; } -export async function cleanupEmbeddedPiRunnerTestWorkspace( - workspace: EmbeddedPiRunnerTestWorkspace | undefined, +export async function cleanupEmbeddedAgentRunnerTestWorkspace( + workspace: EmbeddedAgentRunnerTestWorkspace | undefined, ): Promise { if (!workspace) { return; @@ -32,7 +32,7 @@ export async function cleanupEmbeddedPiRunnerTestWorkspace( await fs.rm(workspace.tempRoot, { recursive: true, force: true }); } -export function createEmbeddedPiRunnerOpenAiConfig(modelIds: string[]): OpenClawConfig { +export function createEmbeddedAgentRunnerOpenAiConfig(modelIds: string[]): OpenClawConfig { return { models: { providers: { diff --git a/src/agents/test-helpers/pi-embedded-runner-e2e-mocks.ts b/src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts similarity index 96% rename from src/agents/test-helpers/pi-embedded-runner-e2e-mocks.ts rename to src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts index 43285a171ce..4760008ad16 100644 --- a/src/agents/test-helpers/pi-embedded-runner-e2e-mocks.ts +++ b/src/agents/test-helpers/embedded-agent-runner-e2e-mocks.ts @@ -55,12 +55,12 @@ export function installEmbeddedRunnerFastRunE2eMocks( ): void { vi.doMock("../harness/selection.js", () => ({ selectAgentHarness: vi.fn((params: { provider?: string }) => ({ - id: params.provider === "codex-cli" ? "codex" : "pi", + id: params.provider === "codex-cli" ? "codex" : "openclaw", label: "Mock agent harness", supports: vi.fn(() => ({ supported: false })), runAttempt: vi.fn(), })), - resolveAgentHarnessPolicy: vi.fn(() => ({ runtime: "pi" })), + resolveAgentHarnessPolicy: vi.fn(() => ({ runtime: "openclaw" })), runAgentHarnessAttempt: (params: unknown) => options.runEmbeddedAttempt(params), })); vi.doMock("../runtime-plan/build.js", () => ({ @@ -131,7 +131,7 @@ export function installEmbeddedRunnerFastRunE2eMocks( }), ), })); - vi.doMock("../pi-embedded-runner/run/attempt.js", () => ({ + vi.doMock("../embedded-agent-runner/run/attempt.js", () => ({ runEmbeddedAttempt: (params: unknown) => options.runEmbeddedAttempt(params), })); vi.doMock("../../plugins/provider-runtime.js", () => ({ diff --git a/src/agents/test-helpers/unsafe-mounted-sandbox.ts b/src/agents/test-helpers/unsafe-mounted-sandbox.ts index 2d8ad84e7eb..e7cb62389ce 100644 --- a/src/agents/test-helpers/unsafe-mounted-sandbox.ts +++ b/src/agents/test-helpers/unsafe-mounted-sandbox.ts @@ -3,8 +3,8 @@ import os from "node:os"; import path from "node:path"; import type { SandboxContext } from "../sandbox.js"; import type { SandboxFsBridge, SandboxResolvedPath } from "../sandbox/fs-bridge.js"; +import { createAgentToolsSandboxContext } from "./agent-tools-sandbox-context.js"; import { createSandboxFsBridgeFromResolver } from "./host-sandbox-fs-bridge.js"; -import { createPiToolsSandboxContext } from "./pi-tools-sandbox-context.js"; function createUnsafeMountedBridge(params: { root: string; @@ -57,7 +57,7 @@ export function createUnsafeMountedSandbox(params: { agentHostRoot: params.agentRoot, workspaceContainerRoot: params.workspaceContainerRoot, }); - return createPiToolsSandboxContext({ + return createAgentToolsSandboxContext({ workspaceDir: params.sandboxRoot, agentWorkspaceDir: params.agentRoot, workspaceAccess: params.workspaceAccess ?? "rw", diff --git a/src/agents/test-helpers/usage-fixtures.ts b/src/agents/test-helpers/usage-fixtures.ts index ae827cbf575..1b40665b13d 100644 --- a/src/agents/test-helpers/usage-fixtures.ts +++ b/src/agents/test-helpers/usage-fixtures.ts @@ -1,4 +1,4 @@ -import type { Usage } from "@earendil-works/pi-ai"; +import type { Usage } from "openclaw/plugin-sdk/llm"; export const ZERO_USAGE_FIXTURE: Usage = { input: 0, diff --git a/src/agents/tool-call-id.test.ts b/src/agents/tool-call-id.test.ts index 7994c94ba84..c350433f5fb 100644 --- a/src/agents/tool-call-id.test.ts +++ b/src/agents/tool-call-id.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; import { castAgentMessages } from "./test-helpers/agent-message-fixtures.js"; import { diff --git a/src/agents/tool-call-id.ts b/src/agents/tool-call-id.ts index 50794126a07..a661273cc84 100644 --- a/src/agents/tool-call-id.ts +++ b/src/agents/tool-call-id.ts @@ -1,6 +1,10 @@ import { createHash } from "node:crypto"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { isAllowedToolCallName, normalizeAllowedToolNames } from "./tool-call-shared.js"; +import type { AgentMessage } from "./runtime/index.js"; +import { + hasUnredactedSessionsSpawnAttachments, + isAllowedToolCallName, + normalizeAllowedToolNames, +} from "./tool-call-shared.js"; export type ToolCallIdMode = "strict" | "strict9"; const NATIVE_ANTHROPIC_TOOL_USE_ID_RE = /^toolu_[A-Za-z0-9_]+$/; diff --git a/src/agents/tool-images.ts b/src/agents/tool-images.ts index 645d334513f..bc56900c854 100644 --- a/src/agents/tool-images.ts +++ b/src/agents/tool-images.ts @@ -1,5 +1,4 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; -import type { ImageContent } from "@earendil-works/pi-ai"; +import type { ImageContent } from "openclaw/plugin-sdk/llm"; import { createSubsystemLogger } from "../logging/subsystem.js"; import { canonicalizeBase64 } from "../media/base64.js"; import { @@ -14,6 +13,7 @@ import { DEFAULT_IMAGE_MAX_DIMENSION_PX, type ImageSanitizationLimits, } from "./image-sanitization.js"; +import type { AgentToolResult } from "./runtime/index.js"; type ToolContentBlock = AgentToolResult["content"][number]; type ImageContentBlock = Extract; diff --git a/src/agents/tool-policy-pipeline.ts b/src/agents/tool-policy-pipeline.ts index c26eac2e202..8094fb0214e 100644 --- a/src/agents/tool-policy-pipeline.ts +++ b/src/agents/tool-policy-pipeline.ts @@ -1,5 +1,5 @@ -import { filterToolsByPolicy } from "./pi-tools.policy.js"; -import type { AnyAgentTool } from "./pi-tools.types.js"; +import { filterToolsByPolicy } from "./agent-tools.policy.js"; +import type { AnyAgentTool } from "./agent-tools.types.js"; import { isKnownCoreToolId } from "./tool-catalog.js"; import { auditToolPolicyFilter } from "./tool-policy-audit.js"; import { diff --git a/src/agents/tool-replay-repair.live.test.ts b/src/agents/tool-replay-repair.live.test.ts index d843cb1c175..9d36abff3a8 100644 --- a/src/agents/tool-replay-repair.live.test.ts +++ b/src/agents/tool-replay-repair.live.test.ts @@ -1,10 +1,12 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { Api, Context, Model } from "@earendil-works/pi-ai"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; +import type { Api, Context, Model } from "openclaw/plugin-sdk/llm"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; import { getRuntimeConfig } from "../config/config.js"; +import { discoverAuthStorage, discoverModels } from "./agent-model-discovery.js"; import { resolveDefaultAgentDir } from "./agent-scope.js"; +import { sanitizeSessionHistory } from "./embedded-agent-runner/replay-history.js"; import { completeSimpleWithTimeout, type CompleteSimpleContent, @@ -16,8 +18,6 @@ import { } from "./live-test-helpers.js"; import { getApiKeyForModel, requireApiKey } from "./model-auth.js"; import { ensureOpenClawModelsJson } from "./models-config.js"; -import { sanitizeSessionHistory } from "./pi-embedded-runner/replay-history.js"; -import { discoverAuthStorage, discoverModels } from "./pi-model-discovery.js"; import { transformTransportMessages } from "./transport-message-transform.js"; const LIVE = isLiveTestEnabled(); @@ -134,7 +134,7 @@ function buildReplayMessages(model: Model): AgentMessage[] { ] as unknown as AgentMessage[]; } -function buildAbortedTransportMessages(model: Model): Context["messages"] { +function buildAbortedTransportMessages(model: Model): Context["messages"] { const now = Date.now(); return [ { @@ -203,7 +203,7 @@ describeLive("tool replay repair live", () => { const agentDir = resolveDefaultAgentDir(cfg); const authStorage = discoverAuthStorage(agentDir); const modelRegistry = discoverModels(authStorage, agentDir); - const model = modelRegistry.find(target.provider, target.modelId) as Model | null; + const model = modelRegistry.find(target.provider, target.modelId) as Model | null; if (!model) { logProgress(`[tool-replay-repair] model missing from registry: ${target.ref}`); @@ -314,7 +314,7 @@ describeLive("tool replay repair live", () => { const agentDir = resolveDefaultAgentDir(cfg); const authStorage = discoverAuthStorage(agentDir); const modelRegistry = discoverModels(authStorage, agentDir); - const model = modelRegistry.find(target.provider, target.modelId) as Model | null; + const model = modelRegistry.find(target.provider, target.modelId) as Model | null; if (!model) { logProgress(`[tool-replay-repair] model missing from registry: ${target.ref}`); diff --git a/src/agents/tool-search.test.ts b/src/agents/tool-search.test.ts index cd18fb383f8..94258887f37 100644 --- a/src/agents/tool-search.test.ts +++ b/src/agents/tool-search.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from "vitest"; import { setPluginToolMeta } from "../plugins/tools.js"; -import { wrapToolWithAbortSignal } from "./pi-tools.abort.js"; +import { wrapToolWithAbortSignal } from "./agent-tools.abort.js"; import { isToolWrappedWithBeforeToolCallHook, wrapToolWithBeforeToolCallHook, -} from "./pi-tools.before-tool-call.js"; +} from "./agent-tools.before-tool-call.js"; import { testing, addClientToolsToToolSearchCatalog, diff --git a/src/agents/tool-search.ts b/src/agents/tool-search.ts index 1551c1b1060..c23dc134d2c 100644 --- a/src/agents/tool-search.ts +++ b/src/agents/tool-search.ts @@ -1,11 +1,5 @@ import { spawn } from "node:child_process"; import os from "node:os"; -import type { - AgentMessage, - AgentToolResult, - AgentToolUpdateCallback, -} from "@earendil-works/pi-agent-core"; -import type { ToolDefinition } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { getPluginToolMeta } from "../plugins/tools.js"; @@ -19,7 +13,9 @@ import { isToolWrappedWithBeforeToolCallHook, type HookContext, wrapToolWithBeforeToolCallHook, -} from "./pi-tools.before-tool-call.js"; +} from "./agent-tools.before-tool-call.js"; +import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback } from "./runtime/index.js"; +import type { ToolDefinition } from "./sessions/index.js"; import { asToolParamsRecord, jsonResult, ToolInputError } from "./tools/common.js"; import type { AnyAgentTool } from "./tools/common.js"; @@ -50,7 +46,7 @@ export type ToolSearchCatalogToolExecutor = (params: { parentToolCallId?: string; input: unknown; signal?: AbortSignal; - onUpdate?: AgentToolUpdateCallback; + onUpdate?: AgentToolUpdateCallback; }) => Promise>; export type ToolSearchTargetTranscriptProjection = { @@ -1027,7 +1023,7 @@ export class ToolSearchRuntime { options?: { parentToolCallId?: string; signal?: AbortSignal; - onUpdate?: AgentToolUpdateCallback; + onUpdate?: AgentToolUpdateCallback; }, ) => { const catalog = resolveCatalog(this.ctx); @@ -1190,7 +1186,7 @@ async function runCodeMode(params: { code: string; config: ToolSearchConfig; signal?: AbortSignal; - onUpdate?: AgentToolUpdateCallback; + onUpdate?: AgentToolUpdateCallback; }) { const runtime = new ToolSearchRuntime(params.ctx, params.config); const logs: string[] = []; @@ -1229,7 +1225,7 @@ async function runCodeModeBridgeRequest( options?: { parentToolCallId?: string; signal?: AbortSignal; - onUpdate?: AgentToolUpdateCallback; + onUpdate?: AgentToolUpdateCallback; }, ): Promise { const values = Array.isArray(args) ? args : []; @@ -1269,7 +1265,7 @@ function runCodeModeChild(params: { parentToolCallId: string; runtime: ToolSearchRuntime; signal?: AbortSignal; - onUpdate?: AgentToolUpdateCallback; + onUpdate?: AgentToolUpdateCallback; }): Promise { return new Promise((resolve, reject) => { const child = spawn(process.execPath, buildCodeModeChildArgs(), { @@ -1444,7 +1440,7 @@ export function createToolSearchTools(ctx: ToolSearchToolContext): AnyAgentTool[ toolCallId: string, args: unknown, signal?: AbortSignal, - onUpdate?: AgentToolUpdateCallback, + onUpdate?: AgentToolUpdateCallback, ): Promise> => jsonResult( await runCodeMode({ toolCallId, ctx, code: readCode(args), config, signal, onUpdate }), @@ -1487,7 +1483,7 @@ export function createToolSearchTools(ctx: ToolSearchToolContext): AnyAgentTool[ _toolCallId: string, args: unknown, signal?: AbortSignal, - onUpdate?: AgentToolUpdateCallback, + onUpdate?: AgentToolUpdateCallback, ): Promise> => { const call = readCallArgs(args); return jsonResult( diff --git a/src/agents/tools-effective-inventory.test.ts b/src/agents/tools-effective-inventory.test.ts index 8eb6e9733b4..556868f18cf 100644 --- a/src/agents/tools-effective-inventory.test.ts +++ b/src/agents/tools-effective-inventory.test.ts @@ -1,7 +1,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; -import type { createOpenClawCodingTools } from "./pi-tools.js"; +import type { createOpenClawCodingTools } from "./agent-tools.js"; import type { AnyAgentTool } from "./tools/common.js"; function mockTool(params: { @@ -44,7 +44,7 @@ vi.mock("./agent-scope.js", async () => { }; }); -vi.mock("./pi-tools.js", () => ({ +vi.mock("./agent-tools.js", () => ({ createOpenClawCodingTools: (options?: Parameters[0]) => effectiveInventoryState.createToolsMock(options), })); @@ -60,7 +60,7 @@ vi.mock("./channel-tools.js", () => ({ effectiveInventoryState.channelMeta[tool.name], })); -vi.mock("./pi-tools.policy.js", () => ({ +vi.mock("./agent-tools.policy.js", () => ({ resolveEffectiveToolPolicy: () => effectiveInventoryState.effectivePolicy, })); diff --git a/src/agents/tools-effective-inventory.ts b/src/agents/tools-effective-inventory.ts index e878ae44849..78c8207a2d8 100644 --- a/src/agents/tools-effective-inventory.ts +++ b/src/agents/tools-effective-inventory.ts @@ -7,10 +7,10 @@ import { normalizeOptionalString, } from "../shared/string-coerce.js"; import { resolveAgentDir, resolveAgentWorkspaceDir, resolveSessionAgentId } from "./agent-scope.js"; +import { createOpenClawCodingTools } from "./agent-tools.js"; +import { resolveEffectiveToolPolicy } from "./agent-tools.policy.js"; import { getChannelAgentToolMeta } from "./channel-tools.js"; import { normalizeStaticProviderModelId } from "./model-ref-shared.js"; -import { createOpenClawCodingTools } from "./pi-tools.js"; -import { resolveEffectiveToolPolicy } from "./pi-tools.policy.js"; import { findNormalizedProviderValue, normalizeProviderId } from "./provider-id.js"; import { summarizeToolDescriptionText } from "./tool-description-summary.js"; import { resolveToolDisplay } from "./tool-display.js"; diff --git a/src/agents/tools/agent-step.test.ts b/src/agents/tools/agent-step.test.ts index 8bdc0d85bea..dc02470894b 100644 --- a/src/agents/tools/agent-step.test.ts +++ b/src/agents/tools/agent-step.test.ts @@ -15,7 +15,7 @@ vi.mock("../run-wait.js", () => ({ runWaitMocks.waitForAgentRunAndReadUpdatedAssistantReply, })); -vi.mock("../pi-bundle-mcp-tools.js", () => ({ +vi.mock("../agent-bundle-mcp-tools.js", () => ({ retireSessionMcpRuntimeForSessionKey: bundleMcpRuntimeMocks.retireSessionMcpRuntimeForSessionKey, })); diff --git a/src/agents/tools/agent-step.ts b/src/agents/tools/agent-step.ts index 63ead75ef05..ccd399da472 100644 --- a/src/agents/tools/agent-step.ts +++ b/src/agents/tools/agent-step.ts @@ -2,8 +2,8 @@ import crypto from "node:crypto"; import { callGateway } from "../../gateway/call.js"; import { annotateInterSessionPromptText } from "../../sessions/input-provenance.js"; import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js"; +import { retireSessionMcpRuntimeForSessionKey } from "../agent-bundle-mcp-tools.js"; import { resolveNestedAgentLaneForSession } from "../lanes.js"; -import { retireSessionMcpRuntimeForSessionKey } from "../pi-bundle-mcp-tools.js"; import { waitForAgentRunAndReadUpdatedAssistantReply } from "../run-wait.js"; export { readLatestAssistantReply } from "../run-wait.js"; diff --git a/src/agents/tools/agents-list-tool.test.ts b/src/agents/tools/agents-list-tool.test.ts index 59ac041be10..016d88cd6f1 100644 --- a/src/agents/tools/agents-list-tool.test.ts +++ b/src/agents/tools/agents-list-tool.test.ts @@ -36,7 +36,7 @@ describe("agents_list tool", () => { agents: { defaults: { model: "anthropic/claude-opus-4.5", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, subagents: { allowAgents: ["codex"] }, }, list: [ @@ -45,14 +45,14 @@ describe("agents_list tool", () => { id: "codex", name: "Codex", model: "openai/gpt-5.5", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: { "openai/gpt-5.5": { agentRuntime: { id: "codex" } }, }, }, ], }, - } satisfies OpenClawConfig); + } as unknown as OpenClawConfig); const result = await createAgentsListTool({ agentSessionKey: "agent:main:main" }).execute( "call", diff --git a/src/agents/tools/chat-history-text.ts b/src/agents/tools/chat-history-text.ts index fca4128fe5c..1ac2d45009c 100644 --- a/src/agents/tools/chat-history-text.ts +++ b/src/agents/tools/chat-history-text.ts @@ -1,6 +1,6 @@ import { extractAssistantTextForPhase } from "../../shared/chat-message-content.js"; import { sanitizeAssistantVisibleTextWithProfile } from "../../shared/text/assistant-visible-text.js"; -import { sanitizeUserFacingText } from "../pi-embedded-helpers/sanitize-user-facing-text.js"; +import { sanitizeUserFacingText } from "../embedded-agent-helpers/sanitize-user-facing-text.js"; export function stripToolMessages(messages: unknown[]): unknown[] { return messages.filter((msg) => { diff --git a/src/agents/tools/common.ts b/src/agents/tools/common.ts index 75d20cba514..ac1b9813781 100644 --- a/src/agents/tools/common.ts +++ b/src/agents/tools/common.ts @@ -1,14 +1,10 @@ -import type { - AgentTool, - AgentToolResult, - AgentToolUpdateCallback, -} from "@earendil-works/pi-agent-core"; import type { TSchema } from "typebox"; import { readLocalFileSafely } from "../../infra/fs-safe.js"; import { detectMime } from "../../media/mime.js"; import { readSnakeCaseParamRaw } from "../../param-key.js"; import { normalizeStringEntries } from "../../shared/string-normalization.js"; import type { ImageSanitizationLimits } from "../image-sanitization.js"; +import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "../runtime/index.js"; import { sanitizeToolResultImages } from "../tool-images.js"; export type AgentToolWithMeta = AgentTool< @@ -24,11 +20,11 @@ type ErasedAgentToolExecute = { toolCallId: string, params: unknown, signal?: AbortSignal, - onUpdate?: AgentToolUpdateCallback, + onUpdate?: AgentToolUpdateCallback, ): Promise>; }; -export type AnyAgentTool = Omit, "execute"> & +export type AnyAgentTool = Omit & ErasedAgentToolExecute & { displaySummary?: string; }; diff --git a/src/agents/tools/gateway-tool-guard-coverage.test.ts b/src/agents/tools/gateway-tool-guard-coverage.test.ts index 500b251d97a..dd4711cdacd 100644 --- a/src/agents/tools/gateway-tool-guard-coverage.test.ts +++ b/src/agents/tools/gateway-tool-guard-coverage.test.ts @@ -276,16 +276,16 @@ describe("gateway config mutation guard coverage", () => { ); }); - it("blocks per-agent embeddedPi override under agents.list[]", () => { + it("blocks per-agent embeddedAgent override under agents.list[]", () => { expectBlocked( { agents: { - list: [{ id: "worker", embeddedPi: { executionContract: "strict-agentic" } }], + list: [{ id: "worker", embeddedAgent: { executionContract: "strict-agentic" } }], }, }, { agents: { - list: [{ id: "worker", embeddedPi: { executionContract: "none" } }], + list: [{ id: "worker", embeddedAgent: { executionContract: "none" } }], }, }, ); diff --git a/src/agents/tools/image-tool.helpers.ts b/src/agents/tools/image-tool.helpers.ts index 58508304624..4276a903bb2 100644 --- a/src/agents/tools/image-tool.helpers.ts +++ b/src/agents/tools/image-tool.helpers.ts @@ -1,10 +1,10 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { estimateBase64DecodedBytes } from "../../media/base64.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; +import { extractAssistantText } from "../embedded-agent-utils.js"; import { isMinimaxVlmProvider } from "../minimax-vlm.js"; import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js"; -import { extractAssistantText } from "../pi-embedded-utils.js"; import { coerceToolModelConfig, type ToolModelConfig } from "./model-config.helpers.js"; export type ImageModelConfig = ToolModelConfig; diff --git a/src/agents/tools/image-tool.test.ts b/src/agents/tools/image-tool.test.ts index 4300e25c2da..21beeab13ca 100644 --- a/src/agents/tools/image-tool.test.ts +++ b/src/agents/tools/image-tool.test.ts @@ -12,8 +12,8 @@ import type { MediaUnderstandingProvider, } from "../../plugin-sdk/media-understanding.js"; import { withFetchPreconnect } from "../../test-utils/fetch-mock.js"; +import { createOpenClawCodingTools } from "../agent-tools.js"; import { minimaxUnderstandImage } from "../minimax-vlm.js"; -import { createOpenClawCodingTools } from "../pi-tools.js"; import type { SandboxFsBridge } from "../sandbox/fs-bridge.js"; import { createHostSandboxFsBridge } from "../test-helpers/host-sandbox-fs-bridge.js"; import { createUnsafeMountedSandbox } from "../test-helpers/unsafe-mounted-sandbox.js"; @@ -34,7 +34,7 @@ type MockOpenClawToolsOptions = { modelHasVision?: boolean; }; -const piToolsHarness = vi.hoisted(() => ({ +const agentToolsHarness = vi.hoisted(() => ({ createStubTool(name: string) { return { name, @@ -74,8 +74,8 @@ vi.mock("../bash-tools.js", async () => { const actual = await vi.importActual("../bash-tools.js"); return { ...actual, - createExecTool: vi.fn(() => piToolsHarness.createStubTool("exec")), - createProcessTool: vi.fn(() => piToolsHarness.createStubTool("process")), + createExecTool: vi.fn(() => agentToolsHarness.createStubTool("exec")), + createProcessTool: vi.fn(() => agentToolsHarness.createStubTool("process")), }; }); @@ -85,14 +85,14 @@ vi.mock("../channel-tools.js", () => ({ })); vi.mock("../apply-patch.js", () => ({ - createApplyPatchTool: vi.fn(() => piToolsHarness.createStubTool("apply_patch")), + createApplyPatchTool: vi.fn(() => agentToolsHarness.createStubTool("apply_patch")), })); -vi.mock("../pi-tools.before-tool-call.js", () => ({ +vi.mock("../agent-tools.before-tool-call.js", () => ({ wrapToolWithBeforeToolCallHook: vi.fn((tool) => tool), })); -vi.mock("../pi-tools.abort.js", () => ({ +vi.mock("../agent-tools.abort.js", () => ({ wrapToolWithAbortSignal: vi.fn((tool) => tool), })); diff --git a/src/agents/tools/media-tool-shared.ts b/src/agents/tools/media-tool-shared.ts index f89b2d46f25..b9250399352 100644 --- a/src/agents/tools/media-tool-shared.ts +++ b/src/agents/tools/media-tool-shared.ts @@ -1,4 +1,4 @@ -import { type Api, type Model } from "@earendil-works/pi-ai"; +import { type Model } from "openclaw/plugin-sdk/llm"; import type { AgentModelConfig } from "../../config/types.agents-shared.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import type { SsrFPolicy } from "../../infra/net/ssrf.js"; @@ -611,19 +611,16 @@ export function resolveModelFromRegistry(params: { modelRegistry: { find: (provider: string, modelId: string) => unknown }; provider: string; modelId: string; -}): Model { +}): Model { const resolvedRef = normalizeModelRef(params.provider, params.modelId, { allowPluginNormalization: false, }); - let model = params.modelRegistry.find( - resolvedRef.provider, - resolvedRef.model, - ) as Model | null; + let model = params.modelRegistry.find(resolvedRef.provider, resolvedRef.model) as Model | null; if (!model && !resolvedRef.model.includes("/")) { model = params.modelRegistry.find( resolvedRef.provider, `${resolvedRef.provider}/${resolvedRef.model}`, - ) as Model | null; + ) as Model | null; } if (!model) { throw new Error(`Unknown model: ${resolvedRef.provider}/${resolvedRef.model}`); @@ -632,7 +629,7 @@ export function resolveModelFromRegistry(params: { } export async function resolveModelRuntimeApiKey(params: { - model: Model; + model: Model; cfg: OpenClawConfig | undefined; agentDir: string; authStorage: { diff --git a/src/agents/tools/nodes-tool-media.ts b/src/agents/tools/nodes-tool-media.ts index 64a23bdcc99..7bcf954b699 100644 --- a/src/agents/tools/nodes-tool-media.ts +++ b/src/agents/tools/nodes-tool-media.ts @@ -1,5 +1,4 @@ import crypto from "node:crypto"; -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { type CameraFacing, cameraTempPath, @@ -17,6 +16,7 @@ import { parseDurationMs } from "../../cli/parse-duration.js"; import { imageMimeFromFormat } from "../../media/mime.js"; import { normalizeLowercaseStringOrEmpty } from "../../shared/string-coerce.js"; import type { ImageSanitizationLimits } from "../image-sanitization.js"; +import type { AgentToolResult } from "../runtime/index.js"; import { sanitizeToolResultImages } from "../tool-images.js"; import type { GatewayCallOptions } from "./gateway.js"; import { callGatewayTool } from "./gateway.js"; diff --git a/src/agents/tools/pdf-native-providers.ts b/src/agents/tools/pdf-native-providers.ts index f489aae1be7..fd46cc84beb 100644 --- a/src/agents/tools/pdf-native-providers.ts +++ b/src/agents/tools/pdf-native-providers.ts @@ -1,6 +1,6 @@ /** * Direct SDK/HTTP calls for providers that support native PDF document input. - * This bypasses pi-ai's content type system which does not have a "document" type. + * This bypasses shared model runtime's content type system which does not have a "document" type. */ import { normalizeProviderTransportWithPlugin } from "../../plugins/provider-runtime.js"; diff --git a/src/agents/tools/pdf-tool.helpers.ts b/src/agents/tools/pdf-tool.helpers.ts index 3f54f68612c..14f4a9e92c7 100644 --- a/src/agents/tools/pdf-tool.helpers.ts +++ b/src/agents/tools/pdf-tool.helpers.ts @@ -1,11 +1,11 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { resolveAgentModelFallbackValues, resolveAgentModelPrimaryValue, } from "../../config/model-input.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { providerSupportsNativePdfDocument } from "../../media-understanding/defaults.js"; -import { extractAssistantText } from "../pi-embedded-utils.js"; +import { extractAssistantText } from "../embedded-agent-utils.js"; export type PdfModelConfig = { primary?: string; fallbacks?: string[] }; diff --git a/src/agents/tools/pdf-tool.test.ts b/src/agents/tools/pdf-tool.test.ts index d0d44c1a9e1..02f42770b5f 100644 --- a/src/agents/tools/pdf-tool.test.ts +++ b/src/agents/tools/pdf-tool.test.ts @@ -5,19 +5,19 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/config.js"; import * as pdfExtractModule from "../../media/pdf-extract.js"; import * as webMedia from "../../media/web-media.js"; +import * as modelDiscovery from "../agent-model-discovery.js"; import type { AuthProfileStore } from "../auth-profiles/types.js"; import * as modelAuth from "../model-auth.js"; import * as modelsConfig from "../models-config.js"; -import * as modelDiscovery from "../pi-model-discovery.js"; import * as pdfNativeProviders from "./pdf-native-providers.js"; import * as pdfModelConfigModule from "./pdf-tool.model-config.js"; import { resetPdfToolAuthEnv, withTempPdfAgentDir } from "./pdf-tool.test-support.js"; const completeMock = vi.hoisted(() => vi.fn()); -vi.mock("@earendil-works/pi-ai", async () => { +vi.mock("openclaw/plugin-sdk/llm", async () => { const actual = - await vi.importActual("@earendil-works/pi-ai"); + await vi.importActual("openclaw/plugin-sdk/llm"); return { ...actual, complete: completeMock, diff --git a/src/agents/tools/pdf-tool.ts b/src/agents/tools/pdf-tool.ts index 4e29316e28a..d0ffba2de44 100644 --- a/src/agents/tools/pdf-tool.ts +++ b/src/agents/tools/pdf-tool.ts @@ -1,4 +1,4 @@ -import { type Context, complete } from "@earendil-works/pi-ai"; +import { type Context, complete } from "openclaw/plugin-sdk/llm"; import { Type } from "typebox"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { diff --git a/src/agents/tools/sessions-send-helpers.ts b/src/agents/tools/sessions-send-helpers.ts index 7e439278b46..d886e282882 100644 --- a/src/agents/tools/sessions-send-helpers.ts +++ b/src/agents/tools/sessions-send-helpers.ts @@ -12,7 +12,7 @@ export { isReplySkip, } from "./sessions-send-tokens.js"; -const DEFAULT_PING_PONG_TURNS = 5; +const DEFAULT_AGENTNG_PONG_TURNS = 5; const MAX_PING_PONG_TURNS = 20; export type AnnounceTarget = { @@ -121,7 +121,7 @@ export function buildAgentToAgentAnnounceContext(params: { export function resolvePingPongTurns(cfg?: OpenClawConfig) { const raw = cfg?.session?.agentToAgent?.maxPingPongTurns; - const fallback = DEFAULT_PING_PONG_TURNS; + const fallback = DEFAULT_AGENTNG_PONG_TURNS; if (typeof raw !== "number" || !Number.isFinite(raw)) { return fallback; } diff --git a/src/agents/tools/tool-runtime.helpers.ts b/src/agents/tools/tool-runtime.helpers.ts index 664b256809d..f3d852d3fd3 100644 --- a/src/agents/tools/tool-runtime.helpers.ts +++ b/src/agents/tools/tool-runtime.helpers.ts @@ -1,7 +1,7 @@ export { getApiKeyForModel, requireApiKey } from "../model-auth.js"; export { runWithImageModelFallback } from "../model-fallback.js"; export { ensureOpenClawModelsJson } from "../models-config.js"; -export { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js"; +export { discoverAuthStorage, discoverModels } from "../agent-model-discovery.js"; export { createSandboxBridgeReadFile, resolveSandboxedBridgeMediaPath, diff --git a/src/agents/transcript-policy.ts b/src/agents/transcript-policy.ts index 99d350a5f8a..72090058dbf 100644 --- a/src/agents/transcript-policy.ts +++ b/src/agents/transcript-policy.ts @@ -6,8 +6,8 @@ import { shouldPreserveThinkingBlocks } from "../plugins/provider-replay-helpers import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js"; import type { ProviderReplayPolicy } from "../plugins/types.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { isGoogleModelApi } from "./embedded-agent-helpers/google.js"; import { normalizeProviderId } from "./model-selection.js"; -import { isGoogleModelApi } from "./pi-embedded-helpers/google.js"; import type { ToolCallIdMode } from "./tool-call-id.js"; export type TranscriptSanitizeMode = "full" | "images-only"; diff --git a/src/agents/transcript-redact.test.ts b/src/agents/transcript-redact.test.ts index fb65e5e475d..b07b07276c2 100644 --- a/src/agents/transcript-redact.test.ts +++ b/src/agents/transcript-redact.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { redactTranscriptMessage } from "./transcript-redact.js"; diff --git a/src/agents/transcript-redact.ts b/src/agents/transcript-redact.ts index 53746a34df2..119ee6a40bb 100644 --- a/src/agents/transcript-redact.ts +++ b/src/agents/transcript-redact.ts @@ -1,4 +1,3 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { readLoggingConfig } from "../logging/config.js"; import { @@ -6,6 +5,7 @@ import { redactSensitiveFieldValue, redactSensitiveText, } from "../logging/redact.js"; +import type { AgentMessage } from "./runtime/index.js"; function resolveTranscriptRedactPatterns(patterns?: string[]) { return patterns && patterns.length > 0 ? [...patterns, ...getDefaultRedactPatterns()] : undefined; diff --git a/src/agents/transport-message-transform.test.ts b/src/agents/transport-message-transform.test.ts index 7fd44f49fcf..453ffa0cd1b 100644 --- a/src/agents/transport-message-transform.test.ts +++ b/src/agents/transport-message-transform.test.ts @@ -1,9 +1,9 @@ -import type { Api, Context, Model } from "@earendil-works/pi-ai"; +import type { Api, Context, Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { transformTransportMessages } from "./transport-message-transform.js"; -function makeModel(api: Api, provider: string, id: string): Model { - return { api, provider, id, input: [], output: [] } as unknown as Model; +function makeModel(api: Api, provider: string, id: string): Model { + return { api, provider, id, input: [], output: [] } as unknown as Model; } type ToolResultMessage = Extract; diff --git a/src/agents/transport-message-transform.ts b/src/agents/transport-message-transform.ts index 2eb2a625a8d..b39a0af8550 100644 --- a/src/agents/transport-message-transform.ts +++ b/src/agents/transport-message-transform.ts @@ -1,4 +1,4 @@ -import type { Api, Context, Model } from "@earendil-works/pi-ai"; +import type { Api, Context, Model } from "openclaw/plugin-sdk/llm"; import { repairToolUseResultPairing } from "./session-transcript-repair.js"; const SYNTHETIC_TOOL_RESULT_APIS = new Set([ @@ -39,10 +39,10 @@ function isFailedAssistantTurn(message: Context["messages"][number]): boolean { export function transformTransportMessages( messages: Context["messages"], - model: Model, + model: Model, normalizeToolCallId?: ( id: string, - targetModel: Model, + targetModel: Model, source: { provider: string; api: Api; model: string }, ) => string, options?: { @@ -134,7 +134,7 @@ export function transformTransportMessages( return replayable; } - // PI's local transform can synthesize missing results, but it does not move + // The local transport transform can synthesize missing results, but it does not move // displaced real results back before an intervening user turn. Shared repair // handles both, while preserving the previous transport behavior of dropping // aborted/error assistant tool-call turns before replaying strict providers. diff --git a/src/agents/transport-params-runtime-contract.test.ts b/src/agents/transport-params-runtime-contract.test.ts index f86fc626d94..c0039419217 100644 --- a/src/agents/transport-params-runtime-contract.test.ts +++ b/src/agents/transport-params-runtime-contract.test.ts @@ -1,5 +1,5 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { Context, Model } from "@earendil-works/pi-ai"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; +import type { Context, Model } from "openclaw/plugin-sdk/llm"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { GPT_PARALLEL_TOOL_CALLS_PAYLOAD_APIS, @@ -8,13 +8,13 @@ import { OPENAI_GPT5_TRANSPORT_DEFAULTS, UNRELATED_TOOL_CALLS_PAYLOAD_APIS, } from "../../test/helpers/agents/transport-params-runtime-contract.js"; +import { createOpenAIThinkingLevelWrapper } from "../llm/providers/stream-wrappers/openai.js"; import { testing as extraParamsTesting, applyExtraParamsToAgent, resolveExtraParams, resolvePreparedExtraParams, -} from "./pi-embedded-runner/extra-params.js"; -import { createOpenAIThinkingLevelWrapper } from "./pi-embedded-runner/openai-stream-wrappers.js"; +} from "./embedded-agent-runner/extra-params.js"; import { supportsGptParallelToolCallsPayload } from "./provider-api-families.js"; beforeEach(() => { @@ -25,7 +25,7 @@ afterEach(() => { extraParamsTesting.resetProviderRuntimeDepsForTest(); }); -describe("transport params runtime contract (Pi/OpenAI path)", () => { +describe("transport params runtime contract (embedded OpenClaw/OpenAI path)", () => { it.each(OPENAI_GPT5_TRANSPORT_DEFAULT_CASES)( "applies OpenAI GPT-5 transport defaults for $provider/$modelId", ({ provider, modelId }) => { diff --git a/src/agents/transport-stream-shared.ts b/src/agents/transport-stream-shared.ts index ac7ba712d52..11d54d62af2 100644 --- a/src/agents/transport-stream-shared.ts +++ b/src/agents/transport-stream-shared.ts @@ -1,4 +1,4 @@ -import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; +import { createAssistantMessageEventStream } from "openclaw/plugin-sdk/llm"; import { redactSensitiveText } from "../logging/redact.js"; import { truncateErrorDetail } from "./provider-http-errors.js"; diff --git a/src/agents/usage.test.ts b/src/agents/usage.test.ts index d69b0c48deb..3641c3392e7 100644 --- a/src/agents/usage.test.ts +++ b/src/agents/usage.test.ts @@ -126,7 +126,7 @@ describe("normalizeUsage", () => { }); it("clamps negative input to zero (pre-subtracted cached_tokens > prompt_tokens)", () => { - // pi-ai OpenAI-format providers subtract cached_tokens from prompt_tokens + // shared model runtime OpenAI-format providers subtract cached_tokens from prompt_tokens // upstream. When cached_tokens exceeds prompt_tokens the result is negative. const usage = normalizeUsage({ input: -4900, diff --git a/src/agents/usage.ts b/src/agents/usage.ts index 22ee5a8d648..41f5f1f96ad 100644 --- a/src/agents/usage.ts +++ b/src/agents/usage.ts @@ -135,7 +135,7 @@ export function normalizeUsage(raw?: UsageLike | null): NormalizedUsage | undefi raw.input_tokens_details?.cached_tokens !== undefined || raw.prompt_tokens_details?.cached_tokens !== undefined; - // Some providers (pi-ai OpenAI-format) pre-subtract cached_tokens from + // Some providers (shared model runtime OpenAI-format) pre-subtract cached_tokens from // prompt/input totals upstream, while OpenAI-style prompt/input aliases // include cached tokens in the reported prompt total. Normalize both cases // to uncached input tokens so downstream prompt-token math does not double- diff --git a/src/agents/utils/ansi.ts b/src/agents/utils/ansi.ts new file mode 100644 index 00000000000..e90fa14d936 --- /dev/null +++ b/src/agents/utils/ansi.ts @@ -0,0 +1,60 @@ +/* + * Portions of this file are derived from: + * - ansi-regex (https://github.com/chalk/ansi-regex) + * - strip-ansi (https://github.com/chalk/strip-ansi) + * + * MIT License + * + * Copyright (c) Sindre Sorhus (https://sindresorhus.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +function ansiRegex({ onlyFirst = false }: { onlyFirst?: boolean } = {}): RegExp { + // Valid string terminator sequences are BEL, ESC\, and 0x9c + const ST = "(?:\\u0007|\\u001B\\u005C|\\u009C)"; + + // OSC sequences only: ESC ] ... ST (non-greedy until the first ST) + const osc = `(?:\\u001B\\][\\s\\S]*?${ST})`; + + // CSI and related: ESC/C1, optional intermediates, optional params (supports ; and :) then final byte + const csi = "[\\u001B\\u009B][[\\]()#;?]*(?:\\d{1,4}(?:[;:]\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]"; + + const pattern = `${osc}|${csi}`; + + return new RegExp(pattern, onlyFirst ? undefined : "g"); +} + +const regex = ansiRegex(); + +export function stripAnsi(value: string): string { + if (typeof value !== "string") { + throw new TypeError(`Expected a \`string\`, got \`${typeof value}\``); + } + + // Fast path: ANSI codes require ESC (7-bit) or CSI (8-bit) introducer + if (!value.includes("\u001B") && !value.includes("\u009B")) { + return value; + } + + // Even though the regex is global, we don't need to reset the `.lastIndex` + // because unlike `.exec()` and `.test()`, `.replace()` does it automatically + // and doing it manually has a performance penalty. + return value.replace(regex, ""); +} diff --git a/src/agents/utils/changelog.ts b/src/agents/utils/changelog.ts new file mode 100644 index 00000000000..99e50cffe85 --- /dev/null +++ b/src/agents/utils/changelog.ts @@ -0,0 +1,103 @@ +import { existsSync, readFileSync } from "node:fs"; + +export interface ChangelogEntry { + major: number; + minor: number; + patch: number; + content: string; +} + +/** + * Parse changelog entries from CHANGELOG.md + * Scans for ## lines and collects content until next ## or EOF + */ +export function parseChangelog(changelogPath: string): ChangelogEntry[] { + if (!existsSync(changelogPath)) { + return []; + } + + try { + const content = readFileSync(changelogPath, "utf-8"); + const lines = content.split("\n"); + const entries: ChangelogEntry[] = []; + + let currentLines: string[] = []; + let currentVersion: { major: number; minor: number; patch: number } | null = null; + + for (const line of lines) { + // Check if this is a version header (## [x.y.z] ...) + if (line.startsWith("## ")) { + // Save previous entry if exists + if (currentVersion && currentLines.length > 0) { + entries.push({ + ...currentVersion, + content: currentLines.join("\n").trim(), + }); + } + + // Try to parse version from this line + const versionMatch = line.match(/##\s+\[?(\d+)\.(\d+)\.(\d+)\]?/); + if (versionMatch) { + currentVersion = { + major: Number.parseInt(versionMatch[1], 10), + minor: Number.parseInt(versionMatch[2], 10), + patch: Number.parseInt(versionMatch[3], 10), + }; + currentLines = [line]; + } else { + // Reset if we can't parse version + currentVersion = null; + currentLines = []; + } + } else if (currentVersion) { + // Collect lines for current version + currentLines.push(line); + } + } + + // Save last entry + if (currentVersion && currentLines.length > 0) { + entries.push({ + ...currentVersion, + content: currentLines.join("\n").trim(), + }); + } + + return entries; + } catch (error) { + console.error(`Warning: Could not parse changelog: ${String(error)}`); + return []; + } +} + +/** + * Compare versions. Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2 + */ +export function compareVersions(v1: ChangelogEntry, v2: ChangelogEntry): number { + if (v1.major !== v2.major) { + return v1.major - v2.major; + } + if (v1.minor !== v2.minor) { + return v1.minor - v2.minor; + } + return v1.patch - v2.patch; +} + +/** + * Get entries newer than lastVersion + */ +export function getNewEntries(entries: ChangelogEntry[], lastVersion: string): ChangelogEntry[] { + // Parse lastVersion + const parts = lastVersion.split(".").map(Number); + const last: ChangelogEntry = { + major: parts[0] || 0, + minor: parts[1] || 0, + patch: parts[2] || 0, + content: "", + }; + + return entries.filter((entry) => compareVersions(entry, last) > 0); +} + +// Re-export getChangelogPath from paths.ts for convenience +export { getChangelogPath } from "../config.js"; diff --git a/src/agents/utils/child-process.ts b/src/agents/utils/child-process.ts new file mode 100644 index 00000000000..eaeb0dac449 --- /dev/null +++ b/src/agents/utils/child-process.ts @@ -0,0 +1,127 @@ +import { + type ChildProcess, + type ChildProcessByStdio, + spawn as nodeSpawn, + spawnSync as nodeSpawnSync, + type SpawnOptions, + type SpawnOptionsWithStdioTuple, + type SpawnSyncOptionsWithStringEncoding, + type SpawnSyncReturns, + type StdioNull, + type StdioPipe, +} from "node:child_process"; +import type { Readable } from "node:stream"; +import crossSpawn from "cross-spawn"; + +const EXIT_STDIO_GRACE_MS = 100; + +export function spawnProcess( + command: string, + args: string[], + options: SpawnOptionsWithStdioTuple, +): ChildProcessByStdio; +export function spawnProcess(command: string, args: string[], options: SpawnOptions): ChildProcess; +export function spawnProcess(command: string, args: string[], options: SpawnOptions): ChildProcess { + return process.platform === "win32" + ? crossSpawn(command, args, options) + : nodeSpawn(command, args, options); +} + +export function spawnProcessSync( + command: string, + args: string[], + options: SpawnSyncOptionsWithStringEncoding, +): SpawnSyncReturns { + return process.platform === "win32" + ? crossSpawn.sync(command, args, options) + : nodeSpawnSync(command, args, options); +} + +/** + * Wait for a child process to terminate without hanging on inherited stdio handles. + * + * On Windows, daemonized descendants can inherit the child's stdout/stderr pipe + * handles. In that case the child emits `exit`, but `close` can hang forever even + * though the original process is already gone. We wait briefly for stdio to end, + * then forcibly stop tracking the inherited handles. + */ +export function waitForChildProcess(child: ChildProcess): Promise { + return new Promise((resolve, reject) => { + let settled = false; + let exited = false; + let exitCode: number | null = null; + let postExitTimer: NodeJS.Timeout | undefined; + let stdoutEnded = child.stdout === null; + let stderrEnded = child.stderr === null; + + const cleanup = () => { + if (postExitTimer) { + clearTimeout(postExitTimer); + postExitTimer = undefined; + } + child.removeListener("error", onError); + child.removeListener("exit", onExit); + child.removeListener("close", onClose); + child.stdout?.removeListener("end", onStdoutEnd); + child.stderr?.removeListener("end", onStderrEnd); + }; + + const finalize = (code: number | null) => { + if (settled) { + return; + } + settled = true; + cleanup(); + child.stdout?.destroy(); + child.stderr?.destroy(); + resolve(code); + }; + + const maybeFinalizeAfterExit = () => { + if (!exited || settled) { + return; + } + if (stdoutEnded && stderrEnded) { + finalize(exitCode); + } + }; + + const onStdoutEnd = () => { + stdoutEnded = true; + maybeFinalizeAfterExit(); + }; + + const onStderrEnd = () => { + stderrEnded = true; + maybeFinalizeAfterExit(); + }; + + const onError = (err: Error) => { + if (settled) { + return; + } + settled = true; + cleanup(); + reject(err); + }; + + const onExit = (code: number | null) => { + exited = true; + exitCode = code; + maybeFinalizeAfterExit(); + if (!settled) { + postExitTimer = setTimeout(() => finalize(code), EXIT_STDIO_GRACE_MS); + } + }; + + const onClose = (code: number | null) => { + finalize(code); + }; + + child.stdout?.once("end", onStdoutEnd); + child.stderr?.once("end", onStderrEnd); + child.once("error", onError); + child.once("exit", onExit); + child.once("close", onClose); + }); +} diff --git a/src/agents/utils/clipboard-image.ts b/src/agents/utils/clipboard-image.ts new file mode 100644 index 00000000000..0cdad1f7e06 --- /dev/null +++ b/src/agents/utils/clipboard-image.ts @@ -0,0 +1,303 @@ +import { spawnSync } from "node:child_process"; +import { randomUUID } from "node:crypto"; +import { readFileSync, unlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { clipboard } from "./clipboard-native.js"; +import { loadPhoton } from "./photon.js"; + +export type ClipboardImage = { + bytes: Uint8Array; + mimeType: string; +}; + +const SUPPORTED_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"] as const; + +const DEFAULT_LIST_TIMEOUT_MS = 1000; +const DEFAULT_READ_TIMEOUT_MS = 3000; +const DEFAULT_POWERSHELL_TIMEOUT_MS = 5000; +const DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024; + +export function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean { + return Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === "wayland"; +} + +function baseMimeType(mimeType: string): string { + return mimeType.split(";")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase(); +} + +export function extensionForImageMimeType(mimeType: string): string | null { + switch (baseMimeType(mimeType)) { + case "image/png": + return "png"; + case "image/jpeg": + return "jpg"; + case "image/webp": + return "webp"; + case "image/gif": + return "gif"; + default: + return null; + } +} + +function selectPreferredImageMimeType(mimeTypes: string[]): string | null { + const normalized = mimeTypes + .map((t) => t.trim()) + .filter(Boolean) + .map((t) => ({ raw: t, base: baseMimeType(t) })); + + for (const preferred of SUPPORTED_IMAGE_MIME_TYPES) { + const match = normalized.find((t) => t.base === preferred); + if (match) { + return match.raw; + } + } + + const anyImage = normalized.find((t) => t.base.startsWith("image/")); + return anyImage?.raw ?? null; +} + +function isSupportedImageMimeType(mimeType: string): boolean { + const base = baseMimeType(mimeType); + return SUPPORTED_IMAGE_MIME_TYPES.some((t) => t === base); +} + +/** + * Convert unsupported image formats to PNG using Photon. + * Returns null if conversion is unavailable or fails. + */ +async function convertToPng(bytes: Uint8Array): Promise { + const photon = await loadPhoton(); + if (!photon) { + return null; + } + + try { + const image = photon.PhotonImage.new_from_byteslice(bytes); + try { + return image.get_bytes(); + } finally { + image.free(); + } + } catch { + return null; + } +} + +function runCommand( + command: string, + args: string[], + options?: { timeoutMs?: number; maxBufferBytes?: number; env?: NodeJS.ProcessEnv }, +): { stdout: Buffer; ok: boolean } { + const timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS; + const maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES; + + const result = spawnSync(command, args, { + timeout: timeoutMs, + maxBuffer: maxBufferBytes, + env: options?.env, + }); + + if (result.error) { + return { ok: false, stdout: Buffer.alloc(0) }; + } + + if (result.status !== 0) { + return { ok: false, stdout: Buffer.alloc(0) }; + } + + const stdout = Buffer.isBuffer(result.stdout) + ? result.stdout + : Buffer.from(result.stdout ?? "", typeof result.stdout === "string" ? "utf-8" : undefined); + + return { ok: true, stdout }; +} + +function readClipboardImageViaWlPaste(): ClipboardImage | null { + const list = runCommand("wl-paste", ["--list-types"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS }); + if (!list.ok) { + return null; + } + + const types = list.stdout + .toString("utf-8") + .split(/\r?\n/) + .map((t) => t.trim()) + .filter(Boolean); + + const selectedType = selectPreferredImageMimeType(types); + if (!selectedType) { + return null; + } + + const data = runCommand("wl-paste", ["--type", selectedType, "--no-newline"]); + if (!data.ok || data.stdout.length === 0) { + return null; + } + + return { bytes: data.stdout, mimeType: baseMimeType(selectedType) }; +} + +function isWSL(env: NodeJS.ProcessEnv = process.env): boolean { + if (env.WSL_DISTRO_NAME || env.WSLENV) { + return true; + } + + try { + const release = readFileSync("/proc/version", "utf-8"); + return /microsoft|wsl/i.test(release); + } catch { + return false; + } +} + +/** + * On WSL, the Linux clipboard (Wayland/X11) does not receive image data from + * Windows screenshots (Win+Shift+S). PowerShell can access the Windows clipboard + * directly, so we use it as a fallback. + */ +function readClipboardImageViaPowerShell(): ClipboardImage | null { + const tmpFile = join(tmpdir(), `openclaw-wsl-clip-${randomUUID()}.png`); + + try { + const winPathResult = runCommand("wslpath", ["-w", tmpFile], { + timeoutMs: DEFAULT_LIST_TIMEOUT_MS, + }); + if (!winPathResult.ok) { + return null; + } + + const winPath = winPathResult.stdout.toString("utf-8").trim(); + if (!winPath) { + return null; + } + + const psQuotedWinPath = winPath.replaceAll("'", "''"); + const psScript = [ + "Add-Type -AssemblyName System.Windows.Forms", + "Add-Type -AssemblyName System.Drawing", + `$path = '${psQuotedWinPath}'`, + "$img = [System.Windows.Forms.Clipboard]::GetImage()", + "if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }", + ].join("; "); + + const result = runCommand("powershell.exe", ["-NoProfile", "-Command", psScript], { + timeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS, + }); + if (!result.ok) { + return null; + } + + const output = result.stdout.toString("utf-8").trim(); + if (output !== "ok") { + return null; + } + + const bytes = readFileSync(tmpFile); + if (bytes.length === 0) { + return null; + } + + return { bytes: new Uint8Array(bytes), mimeType: "image/png" }; + } catch { + return null; + } finally { + try { + unlinkSync(tmpFile); + } catch { + // Ignore cleanup errors. + } + } +} + +function readClipboardImageViaXclip(): ClipboardImage | null { + const targets = runCommand("xclip", ["-selection", "clipboard", "-t", "TARGETS", "-o"], { + timeoutMs: DEFAULT_LIST_TIMEOUT_MS, + }); + + let candidateTypes: string[] = []; + if (targets.ok) { + candidateTypes = targets.stdout + .toString("utf-8") + .split(/\r?\n/) + .map((t) => t.trim()) + .filter(Boolean); + } + + const preferred = candidateTypes.length > 0 ? selectPreferredImageMimeType(candidateTypes) : null; + const tryTypes = preferred + ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] + : [...SUPPORTED_IMAGE_MIME_TYPES]; + + for (const mimeType of tryTypes) { + const data = runCommand("xclip", ["-selection", "clipboard", "-t", mimeType, "-o"]); + if (data.ok && data.stdout.length > 0) { + return { bytes: data.stdout, mimeType: baseMimeType(mimeType) }; + } + } + + return null; +} + +async function readClipboardImageViaNativeClipboard(): Promise { + if (!clipboard || !clipboard.hasImage()) { + return null; + } + + const imageData = await clipboard.getImageBinary(); + if (!imageData || imageData.length === 0) { + return null; + } + + const bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData); + return { bytes, mimeType: "image/png" }; +} + +export async function readClipboardImage(options?: { + env?: NodeJS.ProcessEnv; + platform?: NodeJS.Platform; +}): Promise { + const env = options?.env ?? process.env; + const platform = options?.platform ?? process.platform; + + if (env.TERMUX_VERSION) { + return null; + } + + let image: ClipboardImage | null = null; + + if (platform === "linux") { + const wsl = isWSL(env); + const wayland = isWaylandSession(env); + + if (wayland || wsl) { + image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip(); + } + + if (!image && wsl) { + image = readClipboardImageViaPowerShell(); + } + + if (!image && !wayland) { + image = await readClipboardImageViaNativeClipboard(); + } + } else { + image = await readClipboardImageViaNativeClipboard(); + } + + if (!image) { + return null; + } + + // Convert unsupported formats (e.g., BMP from WSLg) to PNG + if (!isSupportedImageMimeType(image.mimeType)) { + const pngBytes = await convertToPng(image.bytes); + if (!pngBytes) { + return null; + } + return { bytes: pngBytes, mimeType: "image/png" }; + } + + return image; +} diff --git a/src/agents/utils/clipboard-native.ts b/src/agents/utils/clipboard-native.ts new file mode 100644 index 00000000000..ac7c0658e73 --- /dev/null +++ b/src/agents/utils/clipboard-native.ts @@ -0,0 +1,23 @@ +import { createRequire } from "node:module"; + +export type ClipboardModule = { + setText: (text: string) => Promise; + hasImage: () => boolean; + getImageBinary: () => Promise>; +}; + +const require = createRequire(import.meta.url); +let clipboard: ClipboardModule | null = null; + +const hasDisplay = + process.platform !== "linux" || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY); + +if (!process.env.TERMUX_VERSION && hasDisplay) { + try { + clipboard = require("@mariozechner/clipboard") as ClipboardModule; + } catch { + clipboard = null; + } +} + +export { clipboard }; diff --git a/src/agents/utils/clipboard.ts b/src/agents/utils/clipboard.ts new file mode 100644 index 00000000000..546bef1e8ae --- /dev/null +++ b/src/agents/utils/clipboard.ts @@ -0,0 +1,131 @@ +import { execSync, spawn } from "node:child_process"; +import { platform } from "node:os"; +import { isWaylandSession } from "./clipboard-image.js"; +import { clipboard } from "./clipboard-native.js"; + +type NativeClipboardExecOptions = { + input: string; + timeout: number; + stdio: ["pipe", "ignore", "ignore"]; +}; + +function copyToX11Clipboard(options: NativeClipboardExecOptions): void { + try { + execSync("xclip -selection clipboard", options); + } catch { + execSync("xsel --clipboard --input", options); + } +} + +const MAX_OSC52_ENCODED_LENGTH = 100_000; + +function isRemoteSession(env: NodeJS.ProcessEnv = process.env): boolean { + return Boolean(env.SSH_CONNECTION || env.SSH_CLIENT || env.MOSH_CONNECTION); +} + +function emitOsc52(text: string): boolean { + const encoded = Buffer.from(text).toString("base64"); + if (encoded.length > MAX_OSC52_ENCODED_LENGTH) { + return false; + } + process.stdout.write(`\u001b]52;c;${encoded}\x07`); + return true; +} + +export async function copyToClipboard(text: string): Promise { + let copied = false; + + const p = platform(); + + // Prefer direct clipboard writes. Emitting OSC 52 first can make terminals + // write the same native clipboard concurrently with the addon, and very large + // OSC 52 payloads can desynchronize terminal rendering. + // + // On Linux, skip the native addon. The underlying `clipboard-rs` crate is + // X11-only and does not retain selection ownership after `set_text` + // resolves, so on Wayland-only compositors (Hyprland, Niri, ...) and even + // some X11 sessions the call resolves successfully without populating the + // clipboard. The platform tools below (wl-copy, xclip, xsel) properly + // daemonize and keep ownership. + try { + if (clipboard && p !== "linux") { + await clipboard.setText(text); + copied = true; + } + } catch { + // Fall through to platform-specific clipboard tools. + } + + const remote = isRemoteSession(); + if (copied && !remote) { + return; + } + + const options: NativeClipboardExecOptions = { + input: text, + timeout: 5000, + stdio: ["pipe", "ignore", "ignore"], + }; + + if (!copied) { + try { + if (p === "darwin") { + execSync("pbcopy", options); + copied = true; + } else if (p === "win32") { + execSync("clip", options); + copied = true; + } else { + // Linux. Try Termux, Wayland, or X11 clipboard tools. + if (process.env.TERMUX_VERSION) { + try { + execSync("termux-clipboard-set", options); + copied = true; + } catch { + // Fall back to Wayland or X11 tools. + } + } + + if (!copied) { + const hasWaylandDisplay = Boolean(process.env.WAYLAND_DISPLAY); + const hasX11Display = Boolean(process.env.DISPLAY); + const isWayland = isWaylandSession(); + if (isWayland && hasWaylandDisplay) { + try { + // Verify wl-copy exists (spawn errors are async and won't be caught) + execSync("which wl-copy", { stdio: "ignore" }); + // wl-copy with execSync hangs due to fork behavior; use spawn instead + const proc = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"] }); + proc.stdin.on("error", () => { + // Ignore EPIPE errors if wl-copy exits early + }); + proc.stdin.write(text); + proc.stdin.end(); + proc.unref(); + copied = true; + } catch { + if (hasX11Display) { + copyToX11Clipboard(options); + copied = true; + } + } + } else if (hasX11Display) { + copyToX11Clipboard(options); + copied = true; + } + } + } + } catch { + // Fall through to OSC 52 fallback. + } + } + + if (remote || !copied) { + const osc52Copied = emitOsc52(text); + copied = copied || osc52Copied; + } + + if (!copied) { + throw new Error("Failed to copy to clipboard"); + } +} diff --git a/src/agents/utils/exif-orientation.ts b/src/agents/utils/exif-orientation.ts new file mode 100644 index 00000000000..0694af4e0a0 --- /dev/null +++ b/src/agents/utils/exif-orientation.ts @@ -0,0 +1,220 @@ +import type { PhotonImageType } from "./photon.js"; + +type Photon = typeof import("@silvia-odwyer/photon-node"); + +function readOrientationFromTiff(bytes: Uint8Array, tiffStart: number): number { + if (tiffStart + 8 > bytes.length) { + return 1; + } + + const byteOrder = (bytes[tiffStart] << 8) | bytes[tiffStart + 1]; + const le = byteOrder === 0x4949; + + const read16 = (pos: number): number => { + if (le) { + return bytes[pos] | (bytes[pos + 1] << 8); + } + return (bytes[pos] << 8) | bytes[pos + 1]; + }; + + const read32 = (pos: number): number => { + if (le) { + return bytes[pos] | (bytes[pos + 1] << 8) | (bytes[pos + 2] << 16) | (bytes[pos + 3] << 24); + } + return ( + ((bytes[pos] << 24) | (bytes[pos + 1] << 16) | (bytes[pos + 2] << 8) | bytes[pos + 3]) >>> 0 + ); + }; + + const ifdOffset = read32(tiffStart + 4); + const ifdStart = tiffStart + ifdOffset; + if (ifdStart + 2 > bytes.length) { + return 1; + } + + const entryCount = read16(ifdStart); + for (let i = 0; i < entryCount; i++) { + const entryPos = ifdStart + 2 + i * 12; + if (entryPos + 12 > bytes.length) { + return 1; + } + + if (read16(entryPos) === 0x0112) { + const value = read16(entryPos + 8); + return value >= 1 && value <= 8 ? value : 1; + } + } + + return 1; +} + +function findJpegTiffOffset(bytes: Uint8Array): number { + let offset = 2; + while (offset < bytes.length - 1) { + if (bytes[offset] !== 0xff) { + return -1; + } + const marker = bytes[offset + 1]; + if (marker === 0xff) { + offset++; + continue; + } + + if (marker === 0xe1) { + if (offset + 4 >= bytes.length) { + return -1; + } + const segmentStart = offset + 4; + if (segmentStart + 6 > bytes.length) { + return -1; + } + if (!hasExifHeader(bytes, segmentStart)) { + return -1; + } + return segmentStart + 6; + } + + if (offset + 4 > bytes.length) { + return -1; + } + const length = (bytes[offset + 2] << 8) | bytes[offset + 3]; + offset += 2 + length; + } + + return -1; +} + +function findWebpTiffOffset(bytes: Uint8Array): number { + let offset = 12; + while (offset + 8 <= bytes.length) { + const chunkId = String.fromCharCode( + bytes[offset], + bytes[offset + 1], + bytes[offset + 2], + bytes[offset + 3], + ); + const chunkSize = + bytes[offset + 4] | + (bytes[offset + 5] << 8) | + (bytes[offset + 6] << 16) | + (bytes[offset + 7] << 24); + const dataStart = offset + 8; + + if (chunkId === "EXIF") { + if (dataStart + chunkSize > bytes.length) { + return -1; + } + // Some WebP files have "Exif\0\0" prefix before the TIFF header + const tiffStart = + chunkSize >= 6 && hasExifHeader(bytes, dataStart) ? dataStart + 6 : dataStart; + return tiffStart; + } + + // RIFF chunks are padded to even size + offset = dataStart + chunkSize + (chunkSize % 2); + } + + return -1; +} + +function hasExifHeader(bytes: Uint8Array, offset: number): boolean { + return ( + bytes[offset] === 0x45 && + bytes[offset + 1] === 0x78 && + bytes[offset + 2] === 0x69 && + bytes[offset + 3] === 0x66 && + bytes[offset + 4] === 0x00 && + bytes[offset + 5] === 0x00 + ); +} + +function getExifOrientation(bytes: Uint8Array): number { + let tiffOffset = -1; + + // JPEG: starts with FF D8 + if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xd8) { + tiffOffset = findJpegTiffOffset(bytes); + } + // WebP: starts with RIFF....WEBP + else if ( + bytes.length >= 12 && + bytes[0] === 0x52 && + bytes[1] === 0x49 && + bytes[2] === 0x46 && + bytes[3] === 0x46 && + bytes[8] === 0x57 && + bytes[9] === 0x45 && + bytes[10] === 0x42 && + bytes[11] === 0x50 + ) { + tiffOffset = findWebpTiffOffset(bytes); + } + + if (tiffOffset === -1) { + return 1; + } + return readOrientationFromTiff(bytes, tiffOffset); +} + +type DstIndexFn = (x: number, y: number, w: number, h: number) => number; + +function rotate90(photon: Photon, image: PhotonImageType, dstIndex: DstIndexFn): PhotonImageType { + const w = image.get_width(); + const h = image.get_height(); + const src = image.get_raw_pixels(); + const dst = new Uint8Array(src.length); + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const srcIdx = (y * w + x) * 4; + const dstIdx = dstIndex(x, y, w, h) * 4; + dst[dstIdx] = src[srcIdx]; + dst[dstIdx + 1] = src[srcIdx + 1]; + dst[dstIdx + 2] = src[srcIdx + 2]; + dst[dstIdx + 3] = src[srcIdx + 3]; + } + } + + return new photon.PhotonImage(dst, h, w); +} + +// Flip orientations mutate in-place. Rotations return a new image (caller must free the old one if different). +export function applyExifOrientation( + photon: Photon, + image: PhotonImageType, + originalBytes: Uint8Array, +): PhotonImageType { + const orientation = getExifOrientation(originalBytes); + if (orientation === 1) { + return image; + } + + switch (orientation) { + case 2: + photon.fliph(image); + return image; + case 3: + photon.fliph(image); + photon.flipv(image); + return image; + case 4: + photon.flipv(image); + return image; + case 5: { + const rotated = rotate90(photon, image, (x, y, _w, h) => x * h + (h - 1 - y)); + photon.fliph(rotated); + return rotated; + } + case 6: + return rotate90(photon, image, (x, y, _w, h) => x * h + (h - 1 - y)); + case 7: { + const rotated = rotate90(photon, image, (x, y, w, h) => (w - 1 - x) * h + y); + photon.fliph(rotated); + return rotated; + } + case 8: + return rotate90(photon, image, (x, y, w, h) => (w - 1 - x) * h + y); + default: + return image; + } +} diff --git a/src/agents/utils/frontmatter.ts b/src/agents/utils/frontmatter.ts new file mode 100644 index 00000000000..2d68ee2cce1 --- /dev/null +++ b/src/agents/utils/frontmatter.ts @@ -0,0 +1,40 @@ +import { parse } from "yaml"; + +type ParsedFrontmatter> = { + frontmatter: T; + body: string; +}; + +const normalizeNewlines = (value: string): string => + value.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + +const extractFrontmatter = (content: string): { yamlString: string | null; body: string } => { + const normalized = normalizeNewlines(content); + + if (!normalized.startsWith("---")) { + return { yamlString: null, body: normalized }; + } + + const endIndex = normalized.indexOf("\n---", 3); + if (endIndex === -1) { + return { yamlString: null, body: normalized }; + } + + return { + yamlString: normalized.slice(4, endIndex), + body: normalized.slice(endIndex + 4).trim(), + }; +}; + +export const parseFrontmatter = = Record>( + content: string, +): ParsedFrontmatter => { + const { yamlString, body } = extractFrontmatter(content); + if (!yamlString) { + return { frontmatter: {} as T, body }; + } + const parsed = parse(yamlString); + return { frontmatter: (parsed ?? {}) as T, body }; +}; + +export const stripFrontmatter = (content: string): string => parseFrontmatter(content).body; diff --git a/src/agents/utils/fs-watch.ts b/src/agents/utils/fs-watch.ts new file mode 100644 index 00000000000..746dddcfbaa --- /dev/null +++ b/src/agents/utils/fs-watch.ts @@ -0,0 +1,30 @@ +import { type FSWatcher, type WatchListener, watch } from "node:fs"; + +export const FS_WATCH_RETRY_DELAY_MS = 5000; + +export function closeWatcher(watcher: FSWatcher | null | undefined): void { + if (!watcher) { + return; + } + + try { + watcher.close(); + } catch { + // Ignore watcher close errors + } +} + +export function watchWithErrorHandler( + path: string, + listener: WatchListener, + onError: () => void, +): FSWatcher | null { + try { + const watcher = watch(path, listener); + watcher.on("error", onError); + return watcher; + } catch { + onError(); + return null; + } +} diff --git a/src/agents/utils/git.test.ts b/src/agents/utils/git.test.ts new file mode 100644 index 00000000000..d1585bd6812 --- /dev/null +++ b/src/agents/utils/git.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { parseGitUrl } from "./git.js"; + +describe("parseGitUrl", () => { + it("parses ordinary hosted git sources", () => { + expect(parseGitUrl("git:github.com/openclaw/example-plugin")).toMatchObject({ + type: "git", + host: "github.com", + path: "openclaw/example-plugin", + repo: "https://github.com/openclaw/example-plugin", + }); + }); + + it("rejects repository paths that could escape managed checkout roots", () => { + expect(parseGitUrl("git:https://example.com/openclaw/../outside")).toBeNull(); + expect(parseGitUrl("git:git@example.com:openclaw/../outside")).toBeNull(); + expect(parseGitUrl("git:example.com/openclaw/./outside")).toBeNull(); + }); +}); diff --git a/src/agents/utils/git.ts b/src/agents/utils/git.ts new file mode 100644 index 00000000000..8710b976592 --- /dev/null +++ b/src/agents/utils/git.ts @@ -0,0 +1,233 @@ +import hostedGitInfo from "hosted-git-info"; + +/** + * Parsed git URL information. + */ +export type GitSource = { + /** Always "git" for git sources */ + type: "git"; + /** Clone URL (always valid for git clone, without ref suffix) */ + repo: string; + /** Git host domain (e.g., "github.com") */ + host: string; + /** Repository path (e.g., "user/repo") */ + path: string; + /** Git ref (branch, tag, commit) if specified */ + ref?: string; + /** True if ref was specified (package won't be auto-updated) */ + pinned: boolean; +}; + +function splitRef(url: string): { repo: string; ref?: string } { + const scpLikeMatch = url.match(/^git@([^:]+):(.+)$/); + if (scpLikeMatch) { + const pathWithMaybeRef = scpLikeMatch[2] ?? ""; + const refSeparator = pathWithMaybeRef.indexOf("@"); + if (refSeparator < 0) { + return { repo: url }; + } + const repoPath = pathWithMaybeRef.slice(0, refSeparator); + const ref = pathWithMaybeRef.slice(refSeparator + 1); + if (!repoPath || !ref) { + return { repo: url }; + } + return { + repo: `git@${scpLikeMatch[1] ?? ""}:${repoPath}`, + ref, + }; + } + + if (url.includes("://")) { + try { + const parsed = new URL(url); + const pathWithMaybeRef = parsed.pathname.replace(/^\/+/, ""); + const refSeparator = pathWithMaybeRef.indexOf("@"); + if (refSeparator < 0) { + return { repo: url }; + } + const repoPath = pathWithMaybeRef.slice(0, refSeparator); + const ref = pathWithMaybeRef.slice(refSeparator + 1); + if (!repoPath || !ref) { + return { repo: url }; + } + parsed.pathname = `/${repoPath}`; + return { + repo: parsed.toString().replace(/\/$/, ""), + ref, + }; + } catch { + return { repo: url }; + } + } + + const slashIndex = url.indexOf("/"); + if (slashIndex < 0) { + return { repo: url }; + } + const host = url.slice(0, slashIndex); + const pathWithMaybeRef = url.slice(slashIndex + 1); + const refSeparator = pathWithMaybeRef.indexOf("@"); + if (refSeparator < 0) { + return { repo: url }; + } + const repoPath = pathWithMaybeRef.slice(0, refSeparator); + const ref = pathWithMaybeRef.slice(refSeparator + 1); + if (!repoPath || !ref) { + return { repo: url }; + } + return { + repo: `${host}/${repoPath}`, + ref, + }; +} + +function parseGenericGitUrl(url: string): GitSource | null { + const { repo: repoWithoutRef, ref } = splitRef(url); + let repo = repoWithoutRef; + let host = ""; + let path = ""; + + const scpLikeMatch = repoWithoutRef.match(/^git@([^:]+):(.+)$/); + if (scpLikeMatch) { + host = scpLikeMatch[1] ?? ""; + path = scpLikeMatch[2] ?? ""; + } else if ( + repoWithoutRef.startsWith("https://") || + repoWithoutRef.startsWith("http://") || + repoWithoutRef.startsWith("ssh://") || + repoWithoutRef.startsWith("git://") + ) { + try { + const parsed = new URL(repoWithoutRef); + host = parsed.hostname; + path = parsed.pathname.replace(/^\/+/, ""); + } catch { + return null; + } + } else { + const slashIndex = repoWithoutRef.indexOf("/"); + if (slashIndex < 0) { + return null; + } + host = repoWithoutRef.slice(0, slashIndex); + path = repoWithoutRef.slice(slashIndex + 1); + if (!host.includes(".") && host !== "localhost") { + return null; + } + repo = `https://${repoWithoutRef}`; + } + + const normalizedPath = normalizeGitPath(path); + if (!isSafeGitHost(host) || !normalizedPath) { + return null; + } + + return { + type: "git", + repo, + host, + path: normalizedPath, + ref, + pinned: Boolean(ref), + }; +} + +function isSafeGitHost(host: string): boolean { + return ( + Boolean(host) && !host.includes("/") && !host.includes("\\") && host !== "." && host !== ".." + ); +} + +function normalizeGitPath(path: string): string | null { + const normalizedPath = path.replace(/\.git$/, "").replace(/^\/+/, ""); + const segments = normalizedPath.split("/"); + if (segments.length < 2) { + return null; + } + if ( + segments.some( + (segment) => !segment || segment === "." || segment === ".." || segment.includes("\\"), + ) + ) { + return null; + } + return segments.join("/"); +} + +/** + * Parse git source into a GitSource. + * + * Rules: + * - With git: prefix, accept all historical shorthand forms. + * - Without git: prefix, only accept explicit protocol URLs. + */ +export function parseGitUrl(source: string): GitSource | null { + const trimmed = source.trim(); + const hasGitPrefix = trimmed.startsWith("git:"); + const url = hasGitPrefix ? trimmed.slice(4).trim() : trimmed; + + if (!hasGitPrefix && !/^(https?|ssh|git):\/\//i.test(url)) { + return null; + } + + const split = splitRef(url); + + const hostedCandidates = [split.ref ? `${split.repo}#${split.ref}` : undefined, url].filter( + (value): value is string => Boolean(value), + ); + for (const candidate of hostedCandidates) { + const info = hostedGitInfo.fromUrl(candidate); + if (info) { + if (split.ref && info.project?.includes("@")) { + continue; + } + const host = info.domain || ""; + const path = normalizeGitPath(`${info.user}/${info.project}`); + if (!isSafeGitHost(host) || !path) { + continue; + } + const useHttpsPrefix = + !split.repo.startsWith("http://") && + !split.repo.startsWith("https://") && + !split.repo.startsWith("ssh://") && + !split.repo.startsWith("git://") && + !split.repo.startsWith("git@"); + return { + type: "git", + repo: useHttpsPrefix ? `https://${split.repo}` : split.repo, + host, + path, + ref: info.committish || split.ref || undefined, + pinned: Boolean(info.committish || split.ref), + }; + } + } + + const httpsCandidates = [ + split.ref ? `https://${split.repo}#${split.ref}` : undefined, + `https://${url}`, + ].filter((value): value is string => Boolean(value)); + for (const candidate of httpsCandidates) { + const info = hostedGitInfo.fromUrl(candidate); + if (info) { + if (split.ref && info.project?.includes("@")) { + continue; + } + const host = info.domain || ""; + const path = normalizeGitPath(`${info.user}/${info.project}`); + if (!isSafeGitHost(host) || !path) { + continue; + } + return { + type: "git", + repo: `https://${split.repo}`, + host, + path, + ref: info.committish || split.ref || undefined, + pinned: Boolean(info.committish || split.ref), + }; + } + } + + return parseGenericGitUrl(url); +} diff --git a/src/agents/utils/html.ts b/src/agents/utils/html.ts new file mode 100644 index 00000000000..878589cb3e1 --- /dev/null +++ b/src/agents/utils/html.ts @@ -0,0 +1,51 @@ +export interface DecodedHtmlEntity { + text: string; + length: number; +} + +function decodeCodePoint(codePoint: number): string | undefined { + if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff) { + return undefined; + } + return String.fromCodePoint(codePoint); +} + +export function decodeHtmlEntity(entity: string): string | undefined { + switch (entity) { + case "amp": + return "&"; + case "lt": + return "<"; + case "gt": + return ">"; + case "quot": + return '"'; + case "apos": + return "'"; + } + + if (entity.startsWith("#x") || entity.startsWith("#X")) { + return decodeCodePoint(Number.parseInt(entity.slice(2), 16)); + } + + if (entity.startsWith("#")) { + return decodeCodePoint(Number.parseInt(entity.slice(1), 10)); + } + + return undefined; +} + +export function decodeHtmlEntityAt(html: string, index: number): DecodedHtmlEntity | undefined { + const semicolonIndex = html.indexOf(";", index + 1); + if (semicolonIndex === -1 || semicolonIndex - index > 16) { + return undefined; + } + + const entity = html.slice(index + 1, semicolonIndex); + const decoded = decodeHtmlEntity(entity); + if (decoded === undefined) { + return undefined; + } + + return { text: decoded, length: semicolonIndex - index + 1 }; +} diff --git a/src/agents/utils/image-convert.ts b/src/agents/utils/image-convert.ts new file mode 100644 index 00000000000..830a6547350 --- /dev/null +++ b/src/agents/utils/image-convert.ts @@ -0,0 +1,43 @@ +import { applyExifOrientation } from "./exif-orientation.js"; +import { loadPhoton } from "./photon.js"; + +/** + * Convert image to PNG format for terminal display. + * Kitty graphics protocol requires PNG format (f=100). + */ +export async function convertToPng( + base64Data: string, + mimeType: string, +): Promise<{ data: string; mimeType: string } | null> { + // Already PNG, no conversion needed + if (mimeType === "image/png") { + return { data: base64Data, mimeType }; + } + + const photon = await loadPhoton(); + if (!photon) { + // Photon not available, can't convert + return null; + } + + try { + const bytes = new Uint8Array(Buffer.from(base64Data, "base64")); + const rawImage = photon.PhotonImage.new_from_byteslice(bytes); + const image = applyExifOrientation(photon, rawImage, bytes); + if (image !== rawImage) { + rawImage.free(); + } + try { + const pngBuffer = image.get_bytes(); + return { + data: Buffer.from(pngBuffer).toString("base64"), + mimeType: "image/png", + }; + } finally { + image.free(); + } + } catch { + // Conversion failed + return null; + } +} diff --git a/src/agents/utils/image-resize.ts b/src/agents/utils/image-resize.ts new file mode 100644 index 00000000000..0782dfd135e --- /dev/null +++ b/src/agents/utils/image-resize.ts @@ -0,0 +1,189 @@ +import type { ImageContent } from "openclaw/plugin-sdk/llm"; +import { applyExifOrientation } from "./exif-orientation.js"; +import { loadPhoton } from "./photon.js"; + +export interface ImageResizeOptions { + maxWidth?: number; // Default: 2000 + maxHeight?: number; // Default: 2000 + maxBytes?: number; // Default: 4.5MB of base64 payload (below Anthropic's 5MB limit) + jpegQuality?: number; // Default: 80 +} + +export interface ResizedImage { + data: string; // base64 + mimeType: string; + originalWidth: number; + originalHeight: number; + width: number; + height: number; + wasResized: boolean; +} + +// 4.5MB of base64 payload. Provides headroom below Anthropic's 5MB limit. +const DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024; + +const DEFAULT_OPTIONS: Required = { + maxWidth: 2000, + maxHeight: 2000, + maxBytes: DEFAULT_MAX_BYTES, + jpegQuality: 80, +}; + +interface EncodedCandidate { + data: string; + encodedSize: number; + mimeType: string; +} + +function encodeCandidate(buffer: Uint8Array, mimeType: string): EncodedCandidate { + const data = Buffer.from(buffer).toString("base64"); + return { + data, + encodedSize: Buffer.byteLength(data, "utf-8"), + mimeType, + }; +} + +/** + * Resize an image to fit within the specified max dimensions and encoded file size. + * Returns null if the image cannot be resized below maxBytes. + * + * Uses Photon (Rust/WASM) for image processing. If Photon is not available, + * returns null. + * + * Strategy for staying under maxBytes: + * 1. First resize to maxWidth/maxHeight + * 2. Try both PNG and JPEG formats, pick the smaller one + * 3. If still too large, try JPEG with decreasing quality + * 4. If still too large, progressively reduce dimensions until 1x1 + */ +export async function resizeImage( + img: ImageContent, + options?: ImageResizeOptions, +): Promise { + const opts = { ...DEFAULT_OPTIONS, ...options }; + const inputBuffer = Buffer.from(img.data, "base64"); + const inputBase64Size = Buffer.byteLength(img.data, "utf-8"); + + const photon = await loadPhoton(); + if (!photon) { + return null; + } + + let image: ReturnType | undefined; + try { + const inputBytes = new Uint8Array(inputBuffer); + const rawImage = photon.PhotonImage.new_from_byteslice(inputBytes); + image = applyExifOrientation(photon, rawImage, inputBytes); + if (image !== rawImage) { + rawImage.free(); + } + + const originalWidth = image.get_width(); + const originalHeight = image.get_height(); + const format = img.mimeType?.split("/")[1] ?? "png"; + + // Check if already within all limits (dimensions AND encoded size) + if ( + originalWidth <= opts.maxWidth && + originalHeight <= opts.maxHeight && + inputBase64Size < opts.maxBytes + ) { + return { + data: img.data, + mimeType: img.mimeType ?? `image/${format}`, + originalWidth, + originalHeight, + width: originalWidth, + height: originalHeight, + wasResized: false, + }; + } + + // Calculate initial dimensions respecting max limits + let targetWidth = originalWidth; + let targetHeight = originalHeight; + + if (targetWidth > opts.maxWidth) { + targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth); + targetWidth = opts.maxWidth; + } + if (targetHeight > opts.maxHeight) { + targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight); + targetHeight = opts.maxHeight; + } + + function tryEncodings( + width: number, + height: number, + jpegQualities: number[], + ): EncodedCandidate[] { + const resized = photon!.resize(image!, width, height, photon!.SamplingFilter.Lanczos3); + + try { + const candidates: EncodedCandidate[] = [encodeCandidate(resized.get_bytes(), "image/png")]; + for (const quality of jpegQualities) { + candidates.push(encodeCandidate(resized.get_bytes_jpeg(quality), "image/jpeg")); + } + return candidates; + } finally { + resized.free(); + } + } + + const qualitySteps = Array.from(new Set([opts.jpegQuality, 85, 70, 55, 40])); + let currentWidth = targetWidth; + let currentHeight = targetHeight; + + while (true) { + const candidates = tryEncodings(currentWidth, currentHeight, qualitySteps); + for (const candidate of candidates) { + if (candidate.encodedSize < opts.maxBytes) { + return { + data: candidate.data, + mimeType: candidate.mimeType, + originalWidth, + originalHeight, + width: currentWidth, + height: currentHeight, + wasResized: true, + }; + } + } + + if (currentWidth === 1 && currentHeight === 1) { + break; + } + + const nextWidth = currentWidth === 1 ? 1 : Math.max(1, Math.floor(currentWidth * 0.75)); + const nextHeight = currentHeight === 1 ? 1 : Math.max(1, Math.floor(currentHeight * 0.75)); + if (nextWidth === currentWidth && nextHeight === currentHeight) { + break; + } + + currentWidth = nextWidth; + currentHeight = nextHeight; + } + + return null; + } catch { + return null; + } finally { + if (image) { + image.free(); + } + } +} + +/** + * Format a dimension note for resized images. + * This helps the model understand the coordinate mapping. + */ +export function formatDimensionNote(result: ResizedImage): string | undefined { + if (!result.wasResized) { + return undefined; + } + + const scale = result.originalWidth / result.width; + return `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${result.height}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`; +} diff --git a/src/agents/utils/mime.ts b/src/agents/utils/mime.ts new file mode 100644 index 00000000000..994e37a12d9 --- /dev/null +++ b/src/agents/utils/mime.ts @@ -0,0 +1,90 @@ +import { open } from "node:fs/promises"; + +const IMAGE_TYPE_SNIFF_BYTES = 4100; +const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; + +export function detectSupportedImageMimeType(buffer: Uint8Array): string | null { + if (startsWith(buffer, [0xff, 0xd8, 0xff])) { + return buffer[3] === 0xf7 ? null : "image/jpeg"; + } + if (startsWith(buffer, PNG_SIGNATURE)) { + return isPng(buffer) && !isAnimatedPng(buffer) ? "image/png" : null; + } + if (startsWithAscii(buffer, 0, "GIF")) { + return "image/gif"; + } + if (startsWithAscii(buffer, 0, "RIFF") && startsWithAscii(buffer, 8, "WEBP")) { + return "image/webp"; + } + return null; +} + +export async function detectSupportedImageMimeTypeFromFile( + filePath: string, +): Promise { + const fileHandle = await open(filePath, "r"); + try { + const buffer = Buffer.alloc(IMAGE_TYPE_SNIFF_BYTES); + const { bytesRead } = await fileHandle.read(buffer, 0, IMAGE_TYPE_SNIFF_BYTES, 0); + return detectSupportedImageMimeType(buffer.subarray(0, bytesRead)); + } finally { + await fileHandle.close(); + } +} + +function isPng(buffer: Uint8Array): boolean { + return ( + buffer.length >= 16 && + readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && + startsWithAscii(buffer, 12, "IHDR") + ); +} + +function isAnimatedPng(buffer: Uint8Array): boolean { + let offset = PNG_SIGNATURE.length; + while (offset + 8 <= buffer.length) { + const chunkLength = readUint32BE(buffer, offset); + const chunkTypeOffset = offset + 4; + if (startsWithAscii(buffer, chunkTypeOffset, "acTL")) { + return true; + } + if (startsWithAscii(buffer, chunkTypeOffset, "IDAT")) { + return false; + } + + const nextOffset = offset + 8 + chunkLength + 4; + if (nextOffset <= offset || nextOffset > buffer.length) { + return false; + } + offset = nextOffset; + } + return false; +} + +function readUint32BE(buffer: Uint8Array, offset: number): number { + return ( + (buffer[offset] ?? 0) * 0x1000000 + + ((buffer[offset + 1] ?? 0) << 16) + + ((buffer[offset + 2] ?? 0) << 8) + + (buffer[offset + 3] ?? 0) + ); +} + +function startsWith(buffer: Uint8Array, bytes: number[]): boolean { + if (buffer.length < bytes.length) { + return false; + } + return bytes.every((byte, index) => buffer[index] === byte); +} + +function startsWithAscii(buffer: Uint8Array, offset: number, text: string): boolean { + if (buffer.length < offset + text.length) { + return false; + } + for (let index = 0; index < text.length; index++) { + if (buffer[offset + index] !== text.charCodeAt(index)) { + return false; + } + } + return true; +} diff --git a/src/agents/utils/paths.ts b/src/agents/utils/paths.ts new file mode 100644 index 00000000000..c059f8943eb --- /dev/null +++ b/src/agents/utils/paths.ts @@ -0,0 +1,78 @@ +import { realpathSync } from "node:fs"; +import { isAbsolute, relative, resolve as resolvePath, sep } from "node:path"; +import { spawnProcessSync } from "./child-process.js"; + +/** + * Resolve a path to its canonical (real) form, following symlinks. + * Falls back to the raw path if resolution fails (e.g. the target does + * not exist yet), so that callers never crash on missing filesystem + * entries. + */ +export function canonicalizePath(path: string): string { + try { + return realpathSync(path); + } catch { + return path; + } +} + +/** + * Returns true if the value is NOT a package source (npm:, git:, etc.) + * or a URL protocol. Bare names and relative paths without ./ prefix + * are considered local. + */ +export function isLocalPath(value: string): boolean { + const trimmed = value.trim(); + // Known non-local prefixes + if ( + trimmed.startsWith("npm:") || + trimmed.startsWith("git:") || + trimmed.startsWith("github:") || + trimmed.startsWith("http:") || + trimmed.startsWith("https:") || + trimmed.startsWith("ssh:") + ) { + return false; + } + return true; +} + +function resolveAgainstCwd(filePath: string, cwd: string): string { + return isAbsolute(filePath) ? resolvePath(filePath) : resolvePath(cwd, filePath); +} + +export function getCwdRelativePath(filePath: string, cwd: string): string | undefined { + const resolvedCwd = resolvePath(cwd); + const resolvedPath = resolveAgainstCwd(filePath, resolvedCwd); + const relativePath = relative(resolvedCwd, resolvedPath); + const isInsideCwd = + relativePath === "" || + (relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath)); + + return isInsideCwd ? relativePath || "." : undefined; +} + +export function formatPathRelativeToCwdOrAbsolute(filePath: string, cwd: string): string { + const absolutePath = resolveAgainstCwd(filePath, cwd); + return (getCwdRelativePath(absolutePath, cwd) ?? absolutePath).split(sep).join("/"); +} + +export function markPathIgnoredByCloudSync(path: string): void { + const attrs = + process.platform === "darwin" + ? ["com.dropbox.ignored", "com.apple.fileprovider.ignore#P"] + : process.platform === "linux" + ? ["user.com.dropbox.ignored"] + : []; + + for (const attr of attrs) { + if (process.platform === "darwin") { + spawnProcessSync("xattr", ["-w", attr, "1", path], { encoding: "utf-8", stdio: "ignore" }); + } else { + spawnProcessSync("setfattr", ["-n", attr, "-v", "1", path], { + encoding: "utf-8", + stdio: "ignore", + }); + } + } +} diff --git a/src/agents/utils/photon.ts b/src/agents/utils/photon.ts new file mode 100644 index 00000000000..3d97bbb43e7 --- /dev/null +++ b/src/agents/utils/photon.ts @@ -0,0 +1,139 @@ +/** + * Photon image processing wrapper. + * + * This module provides a unified interface to @silvia-odwyer/photon-node that works in: + * 1. Node.js (development, npm run build) + * 2. Bun compiled binaries (standalone distribution) + * + * The challenge: photon-node's CJS entry uses fs.readFileSync(__dirname + '/photon_rs_bg.wasm') + * which bakes the build machine's absolute path into Bun compiled binaries. + * + * Solution: + * 1. Patch fs.readFileSync to redirect missing photon_rs_bg.wasm reads + * 2. Copy photon_rs_bg.wasm next to the executable in standalone binary builds + */ + +import type { PathOrFileDescriptor } from "node:fs"; +import { createRequire } from "node:module"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); +const fs = require("node:fs") as typeof import("fs"); + +// Re-export types from the main package +export type { PhotonImage as PhotonImageType } from "@silvia-odwyer/photon-node"; + +type ReadFileSync = typeof fs.readFileSync; + +const WASM_FILENAME = "photon_rs_bg.wasm"; + +// Lazy-loaded photon module +let photonModule: typeof import("@silvia-odwyer/photon-node") | null = null; +let loadPromise: Promise | null = null; + +function pathOrNull(file: PathOrFileDescriptor): string | null { + if (typeof file === "string") { + return file; + } + if (file instanceof URL) { + return fileURLToPath(file); + } + return null; +} + +function getFallbackWasmPaths(): string[] { + const execDir = path.dirname(process.execPath); + return [ + path.join(execDir, WASM_FILENAME), + path.join(execDir, "photon", WASM_FILENAME), + path.join(process.cwd(), WASM_FILENAME), + ]; +} + +function patchPhotonWasmRead(): () => void { + const originalReadFileSync: ReadFileSync = fs.readFileSync.bind(fs); + const fallbackPaths = getFallbackWasmPaths(); + const mutableFs = fs as { readFileSync: ReadFileSync }; + + const patchedReadFileSync: ReadFileSync = ((...args: Parameters) => { + const [file, options] = args; + const resolvedPath = pathOrNull(file); + + if (resolvedPath?.endsWith(WASM_FILENAME)) { + try { + return originalReadFileSync(...args); + } catch (error) { + const err = error as NodeJS.ErrnoException; + if (err?.code && err.code !== "ENOENT") { + throw error; + } + + for (const fallbackPath of fallbackPaths) { + if (!fs.existsSync(fallbackPath)) { + continue; + } + if (options === undefined) { + return originalReadFileSync(fallbackPath); + } + return originalReadFileSync(fallbackPath, options); + } + + throw error; + } + } + + return originalReadFileSync(...args); + }) as ReadFileSync; + + try { + mutableFs.readFileSync = patchedReadFileSync; + } catch { + Object.defineProperty(fs, "readFileSync", { + value: patchedReadFileSync, + writable: true, + configurable: true, + }); + } + + return () => { + try { + mutableFs.readFileSync = originalReadFileSync; + } catch { + Object.defineProperty(fs, "readFileSync", { + value: originalReadFileSync, + writable: true, + configurable: true, + }); + } + }; +} + +/** + * Load the photon module asynchronously. + * Returns cached module on subsequent calls. + */ +export async function loadPhoton(): Promise { + if (photonModule) { + return photonModule; + } + + if (loadPromise) { + return loadPromise; + } + + loadPromise = (async () => { + const restoreReadFileSync = patchPhotonWasmRead(); + try { + photonModule = await import("@silvia-odwyer/photon-node"); + return photonModule; + } catch { + photonModule = null; + return photonModule; + } finally { + restoreReadFileSync(); + } + })(); + + return loadPromise; +} diff --git a/src/agents/utils/shell.ts b/src/agents/utils/shell.ts new file mode 100644 index 00000000000..7e79e9b3641 --- /dev/null +++ b/src/agents/utils/shell.ts @@ -0,0 +1,225 @@ +import { spawn, spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { delimiter } from "node:path"; +import { getBinDir } from "../config.js"; + +export interface ShellConfig { + shell: string; + args: string[]; +} + +/** + * Find bash executable on PATH (cross-platform) + */ +function findBashOnPath(): string | null { + if (process.platform === "win32") { + // Windows: Use 'where' and verify file exists (where can return non-existent paths) + try { + const result = spawnSync("where", ["bash.exe"], { + encoding: "utf-8", + timeout: 5000, + windowsHide: true, + }); + if (result.status === 0 && result.stdout) { + const firstMatch = result.stdout.trim().split(/\r?\n/)[0]; + if (firstMatch && existsSync(firstMatch)) { + return firstMatch; + } + } + } catch { + // Ignore errors + } + return null; + } + + // Unix: Use 'which' and trust its output (handles Termux and special filesystems) + try { + const result = spawnSync("which", ["bash"], { encoding: "utf-8", timeout: 5000 }); + if (result.status === 0 && result.stdout) { + const firstMatch = result.stdout.trim().split(/\r?\n/)[0]; + if (firstMatch) { + return firstMatch; + } + } + } catch { + // Ignore errors + } + return null; +} + +/** + * Resolve shell configuration based on platform and an optional explicit shell path. + * Resolution order: + * 1. User-specified shellPath + * 2. On Windows: Git Bash in known locations, then bash on PATH + * 3. On Unix: /bin/bash, then bash on PATH, then fallback to sh + */ +export function getShellConfig(customShellPath?: string): ShellConfig { + // 1. Check user-specified shell path + if (customShellPath) { + if (existsSync(customShellPath)) { + return { shell: customShellPath, args: ["-c"] }; + } + throw new Error(`Custom shell path not found: ${customShellPath}`); + } + + if (process.platform === "win32") { + // 2. Try Git Bash in known locations + const paths: string[] = []; + const programFiles = process.env.ProgramFiles; + if (programFiles) { + paths.push(`${programFiles}\\Git\\bin\\bash.exe`); + } + const programFilesX86 = process.env["ProgramFiles(x86)"]; + if (programFilesX86) { + paths.push(`${programFilesX86}\\Git\\bin\\bash.exe`); + } + + for (const path of paths) { + if (existsSync(path)) { + return { shell: path, args: ["-c"] }; + } + } + + // 3. Fallback: search bash.exe on PATH (Cygwin, MSYS2, WSL, etc.) + const bashOnPath = findBashOnPath(); + if (bashOnPath) { + return { shell: bashOnPath, args: ["-c"] }; + } + + throw new Error( + `No bash shell found. Options:\n` + + ` 1. Install Git for Windows: https://git-scm.com/download/win\n` + + ` 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n` + + " 3. Set shellPath in settings.json\n\n" + + `Searched Git Bash in:\n${paths.map((p) => ` ${p}`).join("\n")}`, + ); + } + + // Unix: try /bin/bash, then bash on PATH, then fallback to sh + if (existsSync("/bin/bash")) { + return { shell: "/bin/bash", args: ["-c"] }; + } + + const bashOnPath = findBashOnPath(); + if (bashOnPath) { + return { shell: bashOnPath, args: ["-c"] }; + } + + return { shell: "sh", args: ["-c"] }; +} + +export function getShellEnv(): NodeJS.ProcessEnv { + const binDir = getBinDir(); + const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH"; + const currentPath = process.env[pathKey] ?? ""; + const pathEntries = currentPath.split(delimiter).filter(Boolean); + const hasBinDir = pathEntries.includes(binDir); + const updatedPath = hasBinDir + ? currentPath + : [binDir, currentPath].filter(Boolean).join(delimiter); + + return { + ...process.env, + [pathKey]: updatedPath, + }; +} + +/** + * Sanitize binary output for display/storage. + * Removes characters that crash string-width or cause display issues: + * - Control characters (except tab, newline, carriage return) + * - Lone surrogates + * - Unicode Format characters (crash string-width due to a bug) + * - Characters with undefined code points + */ +export function sanitizeBinaryOutput(str: string): string { + // Use Array.from to properly iterate over code points (not code units) + // This handles surrogate pairs correctly and catches edge cases where + // codePointAt() might return undefined + return Array.from(str) + .filter((char) => { + // Filter out characters that cause string-width to crash + // This includes: + // - Unicode format characters + // - Lone surrogates (already filtered by Array.from) + // - Control chars except \t \n \r + // - Characters with undefined code points + + const code = char.codePointAt(0); + + // Skip if code point is undefined (edge case with invalid strings) + if (code === undefined) { + return false; + } + + // Allow tab, newline, carriage return + if (code === 0x09 || code === 0x0a || code === 0x0d) { + return true; + } + + // Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d) + if (code <= 0x1f) { + return false; + } + + // Filter out Unicode format characters + if (code >= 0xfff9 && code <= 0xfffb) { + return false; + } + + return true; + }) + .join(""); +} + +/** + * Detached child processes must be tracked so they can be killed on parent + * shutdown signals (SIGHUP/SIGTERM). + */ +const trackedDetachedChildPids = new Set(); + +export function trackDetachedChildPid(pid: number): void { + trackedDetachedChildPids.add(pid); +} + +export function untrackDetachedChildPid(pid: number): void { + trackedDetachedChildPids.delete(pid); +} + +export function killTrackedDetachedChildren(): void { + for (const pid of trackedDetachedChildPids) { + killProcessTree(pid); + } + trackedDetachedChildPids.clear(); +} + +/** + * Kill a process and all its children (cross-platform) + */ +export function killProcessTree(pid: number): void { + if (process.platform === "win32") { + // Use taskkill on Windows to kill process tree + try { + spawn("taskkill", ["/F", "/T", "/PID", String(pid)], { + stdio: "ignore", + detached: true, + windowsHide: true, + }); + } catch { + // Ignore errors if taskkill fails + } + } else { + // Use SIGKILL on Unix/Linux/Mac + try { + process.kill(-pid, "SIGKILL"); + } catch { + // Fallback to killing just the child if process group kill fails + try { + process.kill(pid, "SIGKILL"); + } catch { + // Process already dead + } + } + } +} diff --git a/src/agents/utils/sleep.ts b/src/agents/utils/sleep.ts new file mode 100644 index 00000000000..60232f35252 --- /dev/null +++ b/src/agents/utils/sleep.ts @@ -0,0 +1,18 @@ +/** + * Sleep helper that respects abort signal. + */ +export function sleep(ms: number, signal?: AbortSignal): Promise { + return new Promise((resolve, reject) => { + if (signal?.aborted) { + reject(new Error("Aborted")); + return; + } + + const timeout = setTimeout(resolve, ms); + + signal?.addEventListener("abort", () => { + clearTimeout(timeout); + reject(new Error("Aborted")); + }); + }); +} diff --git a/src/agents/utils/syntax-highlight.ts b/src/agents/utils/syntax-highlight.ts new file mode 100644 index 00000000000..40e22176487 --- /dev/null +++ b/src/agents/utils/syntax-highlight.ts @@ -0,0 +1,155 @@ +import hljs from "highlight.js/lib/index.js"; +import { decodeHtmlEntityAt } from "./html.js"; + +export type HighlightFormatter = (text: string) => string; +export type HighlightTheme = Partial>; + +export interface HighlightOptions { + language?: string; + ignoreIllegals?: boolean; + languageSubset?: string[]; + theme?: HighlightTheme; +} + +const SPAN_CLOSE = ""; +const HIGHLIGHT_CLASS_PREFIX = "hljs-"; + +function getScopeFromSpanTag(tag: string): string | undefined { + const match = /\sclass\s*=\s*(?:"([^"]*)"|'([^']*)')/.exec(tag); + const classValue = match?.[1] ?? match?.[2]; + if (!classValue) { + return undefined; + } + + for (const className of classValue.split(/\s+/)) { + if (className.startsWith(HIGHLIGHT_CLASS_PREFIX)) { + return className.slice(HIGHLIGHT_CLASS_PREFIX.length); + } + } + + return undefined; +} + +function getScopeFormatter(scope: string, theme: HighlightTheme): HighlightFormatter | undefined { + const exact = theme[scope]; + if (exact) { + return exact; + } + + const dotIndex = scope.indexOf("."); + if (dotIndex !== -1) { + const prefixFormatter = theme[scope.slice(0, dotIndex)]; + if (prefixFormatter) { + return prefixFormatter; + } + } + + const dashIndex = scope.indexOf("-"); + if (dashIndex !== -1) { + const prefixFormatter = theme[scope.slice(0, dashIndex)]; + if (prefixFormatter) { + return prefixFormatter; + } + } + + return undefined; +} + +function getActiveFormatter( + scopes: Array, + theme: HighlightTheme, +): HighlightFormatter | undefined { + for (let i = scopes.length - 1; i >= 0; i--) { + const scope = scopes[i]; + if (!scope) { + continue; + } + const formatter = getScopeFormatter(scope, theme); + if (formatter) { + return formatter; + } + } + return theme.default; +} + +function isSpanOpenTagStart(html: string, index: number): boolean { + if (!html.startsWith("" || + nextChar === " " || + nextChar === "\t" || + nextChar === "\n" || + nextChar === "\r" + ); +} + +export function renderHighlightedHtml(html: string, theme: HighlightTheme = {}): string { + let output = ""; + let textBuffer = ""; + const scopes: Array = []; + + const flushText = () => { + if (!textBuffer) { + return; + } + const formatter = getActiveFormatter(scopes, theme); + output += formatter ? formatter(textBuffer) : textBuffer; + textBuffer = ""; + }; + + let index = 0; + while (index < html.length) { + if (isSpanOpenTagStart(html, index)) { + const tagEndIndex = html.indexOf(">", index + 5); + if (tagEndIndex !== -1) { + flushText(); + const tag = html.slice(index, tagEndIndex + 1); + const scope = getScopeFromSpanTag(tag); + scopes.push(scope); + index = tagEndIndex + 1; + continue; + } + } + + if (html.startsWith(SPAN_CLOSE, index)) { + flushText(); + if (scopes.length > 0) { + scopes.pop(); + } + index += SPAN_CLOSE.length; + continue; + } + + if (html[index] === "&") { + const decoded = decodeHtmlEntityAt(html, index); + if (decoded) { + textBuffer += decoded.text; + index += decoded.length; + continue; + } + } + + textBuffer += html[index]; + index++; + } + + flushText(); + return output; +} + +export function highlight(code: string, options: HighlightOptions = {}): string { + const html = options.language + ? hljs.highlight(code, { + language: options.language, + ignoreIllegals: options.ignoreIllegals, + }).value + : hljs.highlightAuto(code, options.languageSubset).value; + return renderHighlightedHtml(html, options.theme); +} + +export function supportsLanguage(name: string): boolean { + return hljs.getLanguage(name) !== undefined; +} diff --git a/src/agents/utils/tools-manager.ts b/src/agents/utils/tools-manager.ts new file mode 100644 index 00000000000..5964115251e --- /dev/null +++ b/src/agents/utils/tools-manager.ts @@ -0,0 +1,415 @@ +import { type SpawnSyncReturns, spawnSync } from "node:child_process"; +import { + chmodSync, + createWriteStream, + existsSync, + mkdirSync, + readdirSync, + renameSync, + rmSync, +} from "node:fs"; +import { arch, platform } from "node:os"; +import { join } from "node:path"; +import { Readable } from "node:stream"; +import { pipeline } from "node:stream/promises"; +import type { ReadableStream as NodeReadableStream } from "node:stream/web"; +import chalk from "chalk"; +import { APP_NAME, getBinDir } from "../config.js"; + +const TOOLS_DIR = getBinDir(); +const NETWORK_TIMEOUT_MS = 10_000; +const DOWNLOAD_TIMEOUT_MS = 120_000; + +function isOfflineModeEnabled(): boolean { + const value = process.env.OPENCLAW_OFFLINE; + if (!value) { + return false; + } + return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes"; +} + +interface ToolConfig { + name: string; + repo: string; // GitHub repo (e.g., "sharkdp/fd") + binaryName: string; // Name of the binary inside the archive + systemBinaryNames?: string[]; // Alternative system command names to try before downloading + tagPrefix: string; // Prefix for tags (e.g., "v" for v1.0.0, "" for 1.0.0) + getAssetName: (version: string, plat: string, architecture: string) => string | null; +} + +const TOOLS: Record = { + fd: { + name: "fd", + repo: "sharkdp/fd", + binaryName: "fd", + systemBinaryNames: ["fd", "fdfind"], + tagPrefix: "v", + getAssetName: (version, plat, architecture) => { + if (plat === "darwin") { + const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; + return `fd-v${version}-${archStr}-apple-darwin.tar.gz`; + } else if (plat === "linux") { + const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; + return `fd-v${version}-${archStr}-unknown-linux-gnu.tar.gz`; + } else if (plat === "win32") { + const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; + return `fd-v${version}-${archStr}-pc-windows-msvc.zip`; + } + return null; + }, + }, + rg: { + name: "ripgrep", + repo: "BurntSushi/ripgrep", + binaryName: "rg", + tagPrefix: "", + getAssetName: (version, plat, architecture) => { + if (plat === "darwin") { + const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; + return `ripgrep-${version}-${archStr}-apple-darwin.tar.gz`; + } else if (plat === "linux") { + if (architecture === "arm64") { + return `ripgrep-${version}-aarch64-unknown-linux-gnu.tar.gz`; + } + return `ripgrep-${version}-x86_64-unknown-linux-musl.tar.gz`; + } else if (plat === "win32") { + const archStr = architecture === "arm64" ? "aarch64" : "x86_64"; + return `ripgrep-${version}-${archStr}-pc-windows-msvc.zip`; + } + return null; + }, + }, +}; + +// Check if a command exists in PATH by trying to run it +function commandExists(cmd: string): boolean { + try { + const result = spawnSync(cmd, ["--version"], { stdio: "pipe" }); + // Check for ENOENT error (command not found) + return result.error === undefined || result.error === null; + } catch { + return false; + } +} + +// Get the path to a tool (system-wide or in our tools dir) +export function getToolPath(tool: "fd" | "rg"): string | null { + const config = TOOLS[tool]; + if (!config) { + return null; + } + + // Check our tools directory first + const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : "")); + if (existsSync(localPath)) { + return localPath; + } + + // Check system PATH - if found, just return the command name (it's in PATH) + const systemBinaryNames = config.systemBinaryNames ?? [config.binaryName]; + for (const systemBinaryName of systemBinaryNames) { + if (commandExists(systemBinaryName)) { + return systemBinaryName; + } + } + + return null; +} + +// Fetch latest release version from GitHub +async function getLatestVersion(repo: string): Promise { + const response = await fetch(`https://api.github.com/repos/${repo}/releases/latest`, { + headers: { "User-Agent": `${APP_NAME}-coding-agent` }, + signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS), + }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status}`); + } + + const data = (await response.json()) as { tag_name: string }; + return data.tag_name.replace(/^v/, ""); +} + +// Download a file from URL +async function downloadFile(url: string, dest: string): Promise { + const response = await fetch(url, { + signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS), + }); + + if (!response.ok) { + throw new Error(`Failed to download: ${response.status}`); + } + + if (!response.body) { + throw new Error("No response body"); + } + + const fileStream = createWriteStream(dest); + await pipeline(Readable.fromWeb(response.body as NodeReadableStream), fileStream); +} + +function findBinaryRecursively(rootDir: string, binaryFileName: string): string | null { + const stack: string[] = [rootDir]; + + while (stack.length > 0) { + const currentDir = stack.pop(); + if (!currentDir) { + continue; + } + + const entries = readdirSync(currentDir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = join(currentDir, entry.name); + if (entry.isFile() && entry.name === binaryFileName) { + return fullPath; + } + if (entry.isDirectory()) { + stack.push(fullPath); + } + } + } + + return null; +} + +function formatSpawnFailure(result: SpawnSyncReturns): string { + if (result.error?.message) { + return result.error.message; + } + const stderr = result.stderr?.toString().trim(); + if (stderr) { + return stderr; + } + const stdout = result.stdout?.toString().trim(); + if (stdout) { + return stdout; + } + return `exit status ${result.status ?? "unknown"}`; +} + +function runExtractionCommand(command: string, args: string[]): string | null { + const result = spawnSync(command, args, { stdio: "pipe" }); + if (!result.error && result.status === 0) { + return null; + } + return `${command}: ${formatSpawnFailure(result)}`; +} + +function extractTarGzArchive(archivePath: string, extractDir: string, assetName: string): void { + const failure = runExtractionCommand("tar", ["xzf", archivePath, "-C", extractDir]); + if (failure) { + throw new Error(`Failed to extract ${assetName}: ${failure}`); + } +} + +function getWindowsTarCommand(): string { + const systemRoot = process.env.SystemRoot ?? process.env.WINDIR; + if (systemRoot) { + const systemTar = join(systemRoot, "System32", "tar.exe"); + if (existsSync(systemTar)) { + return systemTar; + } + } + return "tar.exe"; +} + +function extractZipArchive(archivePath: string, extractDir: string, assetName: string): void { + const failures: string[] = []; + + if (platform() === "win32") { + // Windows ships bsdtar as tar.exe, which supports zip files. Prefer the + // System32 binary over Git Bash's GNU tar, which does not handle zip archives. + const tarFailure = runExtractionCommand(getWindowsTarCommand(), [ + "xf", + archivePath, + "-C", + extractDir, + ]); + if (!tarFailure) { + return; + } + failures.push(tarFailure); + + const script = + "& { param($archive, $destination) $ErrorActionPreference = 'Stop'; Expand-Archive -LiteralPath $archive -DestinationPath $destination -Force }"; + const powershellFailure = runExtractionCommand("powershell.exe", [ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-ExecutionPolicy", + "Bypass", + "-Command", + script, + archivePath, + extractDir, + ]); + if (!powershellFailure) { + return; + } + failures.push(powershellFailure); + } else { + const unzipFailure = runExtractionCommand("unzip", ["-q", archivePath, "-d", extractDir]); + if (!unzipFailure) { + return; + } + failures.push(unzipFailure); + + const tarFailure = runExtractionCommand("tar", ["xf", archivePath, "-C", extractDir]); + if (!tarFailure) { + return; + } + failures.push(tarFailure); + } + + throw new Error(`Failed to extract ${assetName}: ${failures.join("; ")}`); +} + +// Download and install a tool +async function downloadTool(tool: "fd" | "rg"): Promise { + const config = TOOLS[tool]; + if (!config) { + throw new Error(`Unknown tool: ${tool}`); + } + + const plat = platform(); + const architecture = arch(); + + // Get latest version + let version = await getLatestVersion(config.repo); + if (tool === "fd" && plat === "darwin" && architecture === "x64") { + version = "10.3.0"; + } + + // Get asset name for this platform + const assetName = config.getAssetName(version, plat, architecture); + if (!assetName) { + throw new Error(`Unsupported platform: ${plat}/${architecture}`); + } + + // Create tools directory + mkdirSync(TOOLS_DIR, { recursive: true }); + + const downloadUrl = `https://github.com/${config.repo}/releases/download/${config.tagPrefix}${version}/${assetName}`; + const archivePath = join(TOOLS_DIR, assetName); + const binaryExt = plat === "win32" ? ".exe" : ""; + const binaryPath = join(TOOLS_DIR, config.binaryName + binaryExt); + + // Download + await downloadFile(downloadUrl, archivePath); + + // Extract into a unique temp directory. fd and rg downloads can run concurrently + // during startup, so sharing a fixed directory causes races. + const extractDir = join( + TOOLS_DIR, + `extract_tmp_${config.binaryName}_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`, + ); + mkdirSync(extractDir, { recursive: true }); + + try { + if (assetName.endsWith(".tar.gz")) { + extractTarGzArchive(archivePath, extractDir, assetName); + } else if (assetName.endsWith(".zip")) { + extractZipArchive(archivePath, extractDir, assetName); + } else { + throw new Error(`Unsupported archive format: ${assetName}`); + } + + // Find the binary in extracted files. Some archives contain files directly + // at root, others nest under a versioned subdirectory. + const binaryFileName = config.binaryName + binaryExt; + const extractedDir = join(extractDir, assetName.replace(/\.(tar\.gz|zip)$/, "")); + const extractedBinaryCandidates = [ + join(extractedDir, binaryFileName), + join(extractDir, binaryFileName), + ]; + let extractedBinary = extractedBinaryCandidates.find((candidate) => existsSync(candidate)); + + if (!extractedBinary) { + extractedBinary = findBinaryRecursively(extractDir, binaryFileName) ?? undefined; + } + + if (extractedBinary) { + renameSync(extractedBinary, binaryPath); + } else { + throw new Error( + `Binary not found in archive: expected ${binaryFileName} under ${extractDir}`, + ); + } + + // Make executable (Unix only) + if (plat !== "win32") { + chmodSync(binaryPath, 0o755); + } + } finally { + // Cleanup + rmSync(archivePath, { force: true }); + rmSync(extractDir, { recursive: true, force: true }); + } + + return binaryPath; +} + +// Termux package names for tools +const TERMUX_PACKAGES: Record = { + fd: "fd", + rg: "ripgrep", +}; + +// Ensure a tool is available, downloading if necessary +// Returns the path to the tool, or null if unavailable +export async function ensureTool( + tool: "fd" | "rg", + silent: boolean = false, +): Promise { + const existingPath = getToolPath(tool); + if (existingPath) { + return existingPath; + } + + const config = TOOLS[tool]; + if (!config) { + return undefined; + } + + if (isOfflineModeEnabled()) { + if (!silent) { + console.log( + chalk.yellow(`${config.name} not found. Offline mode enabled, skipping download.`), + ); + } + return undefined; + } + + // On Android/Termux, Linux binaries don't work due to Bionic libc incompatibility. + // Users must install via pkg. + if (platform() === "android") { + const pkgName = TERMUX_PACKAGES[tool] ?? tool; + if (!silent) { + console.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`)); + } + return undefined; + } + + // Tool not found - download it + if (!silent) { + console.log(chalk.dim(`${config.name} not found. Downloading...`)); + } + + try { + const path = await downloadTool(tool); + if (!silent) { + console.log(chalk.dim(`${config.name} installed to ${path}`)); + } + return path; + } catch (e) { + if (!silent) { + console.log( + chalk.yellow( + `Failed to download ${config.name}: ${e instanceof Error ? e.message : String(e)}`, + ), + ); + } + return undefined; + } +} diff --git a/src/agents/xai.live.test.ts b/src/agents/xai.live.test.ts index de8a3c84554..4c559634004 100644 --- a/src/agents/xai.live.test.ts +++ b/src/agents/xai.live.test.ts @@ -1,16 +1,16 @@ -import { completeSimple, getModel, streamSimple } from "@earendil-works/pi-ai"; +import { completeSimple, getModel, streamSimple } from "openclaw/plugin-sdk/llm"; import { Type } from "typebox"; import { describe, expect, it } from "vitest"; +import { + isBillingErrorMessage, + isOverloadedErrorMessage, +} from "./embedded-agent-helpers/failover-matches.js"; +import { applyExtraParamsToAgent } from "./embedded-agent-runner.js"; import { createSingleUserPromptMessage, extractNonEmptyAssistantText, isLiveTestEnabled, } from "./live-test-helpers.js"; -import { - isBillingErrorMessage, - isOverloadedErrorMessage, -} from "./pi-embedded-helpers/failover-matches.js"; -import { applyExtraParamsToAgent } from "./pi-embedded-runner.js"; import { createWebSearchTool } from "./tools/web-search.js"; const XAI_KEY = process.env.XAI_API_KEY ?? ""; diff --git a/src/agents/zai.live.test.ts b/src/agents/zai.live.test.ts index 32dbe3ef11f..e1012b9e5ae 100644 --- a/src/agents/zai.live.test.ts +++ b/src/agents/zai.live.test.ts @@ -1,4 +1,4 @@ -import { completeSimple, getModel } from "@earendil-works/pi-ai"; +import { completeSimple, getModel } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; import { createSingleUserPromptMessage, diff --git a/src/auto-reply/fallback-state.ts b/src/auto-reply/fallback-state.ts index aa39c68fcf3..aada9119ad7 100644 --- a/src/auto-reply/fallback-state.ts +++ b/src/auto-reply/fallback-state.ts @@ -1,5 +1,5 @@ +import { formatRawAssistantErrorForUi } from "../agents/embedded-agent-helpers.js"; import { areRuntimeModelRefsEquivalent } from "../agents/model-runtime-aliases.js"; -import { formatRawAssistantErrorForUi } from "../agents/pi-embedded-helpers.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { FallbackNoticeState } from "../status/fallback-notice-state.js"; import { formatProviderModelRef } from "./model-runtime.js"; diff --git a/src/auto-reply/get-reply-options.types.ts b/src/auto-reply/get-reply-options.types.ts index 6613bae4503..fb56a128b63 100644 --- a/src/auto-reply/get-reply-options.types.ts +++ b/src/auto-reply/get-reply-options.types.ts @@ -1,4 +1,4 @@ -import type { ImageContent } from "@earendil-works/pi-ai"; +import type { ImageContent } from "openclaw/plugin-sdk/llm"; import type { PromptImageOrderEntry } from "../media/prompt-image-order.js"; import type { UserTurnTranscriptRecorder } from "../sessions/user-turn-transcript.js"; import type { ReplyPayload } from "./reply-payload.js"; diff --git a/src/auto-reply/handoff-summarizer.ts b/src/auto-reply/handoff-summarizer.ts index 6e085703f56..f4e511eacd8 100644 --- a/src/auto-reply/handoff-summarizer.ts +++ b/src/auto-reply/handoff-summarizer.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../agents/runtime/index.js"; export interface HandoffSnapshot { summary: string; diff --git a/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts b/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts index 13991573faf..d54e634d549 100644 --- a/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts +++ b/src/auto-reply/reply.directive.directive-behavior.e2e-harness.ts @@ -10,12 +10,12 @@ import type { ProviderPlugin } from "../plugins/types.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { clearSessionAuthProfileOverrideMock, - compactEmbeddedPiSessionMock, + compactEmbeddedAgentSessionMock, loadModelCatalogMock, resolveCommandSecretRefsViaGatewayMock, resolveSessionAuthProfileOverrideMock, runDirectiveBehaviorReplyAgent, - runEmbeddedPiAgentMock, + runEmbeddedAgentMock, runDirectiveBehaviorPreparedReply, runPreparedReplyMock, runReplyAgentMock, @@ -97,9 +97,9 @@ export function installDirectiveBehaviorE2EHooks() { resetSystemEventsForTest(); resetPluginRuntimeStateForTest(); setActivePluginRegistry(createDirectiveBehaviorProviderRegistry()); - compactEmbeddedPiSessionMock.mockReset(); - compactEmbeddedPiSessionMock.mockResolvedValue({ payloads: [], meta: {} }); - runEmbeddedPiAgentMock.mockReset(); + compactEmbeddedAgentSessionMock.mockReset(); + compactEmbeddedAgentSessionMock.mockResolvedValue({ payloads: [], meta: {} }); + runEmbeddedAgentMock.mockReset(); loadModelCatalogMock.mockReset(); loadModelCatalogMock.mockResolvedValue(DEFAULT_TEST_MODEL_CATALOG); resolveCommandSecretRefsViaGatewayMock.mockReset(); diff --git a/src/auto-reply/reply.directive.directive-behavior.e2e-mocks.ts b/src/auto-reply/reply.directive.directive-behavior.e2e-mocks.ts index 4146a466bd4..10d8ccb2ffb 100644 --- a/src/auto-reply/reply.directive.directive-behavior.e2e-mocks.ts +++ b/src/auto-reply/reply.directive.directive-behavior.e2e-mocks.ts @@ -1,7 +1,7 @@ import { vi, type Mock } from "vitest"; -export const runEmbeddedPiAgentMock: Mock = vi.fn(); -export const compactEmbeddedPiSessionMock: Mock = vi.fn(); +export const runEmbeddedAgentMock: Mock = vi.fn(); +export const compactEmbeddedAgentSessionMock: Mock = vi.fn(); export const loadModelCatalogMock: Mock = vi.fn(); export const resolveCommandSecretRefsViaGatewayMock: Mock = vi.fn(); export const clearSessionAuthProfileOverrideMock: Mock = vi.fn(); @@ -38,7 +38,7 @@ function normalizeReplyAgentPayload(payload: Record, params: un } async function runMockedReplyAgent(runParams: unknown, params: unknown) { - const result = await runEmbeddedPiAgentMock(runParams); + const result = await runEmbeddedAgentMock(runParams); const payloadsRaw = objectRecord(result)?.payloads; const payloads = Array.isArray(payloadsRaw) ? payloadsRaw.flatMap((payload) => { @@ -80,26 +80,26 @@ export async function runDirectiveBehaviorPreparedReply(params: unknown) { export const runPreparedReplyMock: Mock = vi.fn(runDirectiveBehaviorPreparedReply); -vi.mock("../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - compactEmbeddedPiSession: (...args: unknown[]) => compactEmbeddedPiSessionMock(...args), - runEmbeddedPiAgent: (...args: unknown[]) => runEmbeddedPiAgentMock(...args), - queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), +vi.mock("../agents/embedded-agent.js", () => ({ + abortEmbeddedAgentRun: vi.fn().mockReturnValue(false), + compactEmbeddedAgentSession: (...args: unknown[]) => compactEmbeddedAgentSessionMock(...args), + runEmbeddedAgent: (...args: unknown[]) => runEmbeddedAgentMock(...args), + queueEmbeddedAgentMessage: vi.fn().mockReturnValue(false), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunActive: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunStreaming: vi.fn().mockReturnValue(false), })); -vi.mock("../agents/pi-embedded.runtime.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - compactEmbeddedPiSession: (...args: unknown[]) => compactEmbeddedPiSessionMock(...args), - runEmbeddedPiAgent: (...args: unknown[]) => runEmbeddedPiAgentMock(...args), - queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), +vi.mock("../agents/embedded-agent.runtime.js", () => ({ + abortEmbeddedAgentRun: vi.fn().mockReturnValue(false), + compactEmbeddedAgentSession: (...args: unknown[]) => compactEmbeddedAgentSessionMock(...args), + runEmbeddedAgent: (...args: unknown[]) => runEmbeddedAgentMock(...args), + queueEmbeddedAgentMessage: vi.fn().mockReturnValue(false), resolveActiveEmbeddedRunSessionId: vi.fn().mockReturnValue(undefined), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), - waitForEmbeddedPiRunEnd: vi.fn().mockResolvedValue(true), + isEmbeddedAgentRunActive: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunStreaming: vi.fn().mockReturnValue(false), + waitForEmbeddedAgentRunEnd: vi.fn().mockResolvedValue(true), })); vi.mock("../agents/model-catalog.js", () => ({ diff --git a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts index c81fdc78efd..e6d148d4ce5 100644 --- a/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts +++ b/src/auto-reply/reply.directive.directive-behavior.shows-current-verbose-level-verbose-has-no.test.ts @@ -4,7 +4,7 @@ import type { ModelAliasIndex } from "../agents/model-selection.js"; import type { OpenClawConfig } from "../config/config.js"; import type { SessionEntry } from "../config/sessions.js"; import { installDirectiveBehaviorE2EHooks } from "./reply.directive.directive-behavior.e2e-harness.js"; -import { runEmbeddedPiAgentMock } from "./reply.directive.directive-behavior.e2e-mocks.js"; +import { runEmbeddedAgentMock } from "./reply.directive.directive-behavior.e2e-mocks.js"; import { handleDirectiveOnly } from "./reply/directive-handling.impl.js"; import type { HandleDirectiveOnlyParams } from "./reply/directive-handling.params.js"; import { parseInlineDirectives } from "./reply/directive-handling.parse.js"; @@ -133,7 +133,7 @@ describe("directive behavior", () => { expect(execText).toContain( "Options: host=auto|sandbox|gateway|node, security=deny|allowlist|full, ask=off|on-miss|always, node=.", ); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); it("treats /fast status like the no-argument status query", async () => { const { text: statusText } = await runDirectiveStatus("/fast status", { @@ -155,7 +155,7 @@ describe("directive behavior", () => { expect(statusText).toContain("Current fast mode: on (config)"); expect(statusText).toContain("Options: status, on, off, default."); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); it("enforces per-agent elevated restrictions and status visibility", async () => { const { text: deniedText } = await runDirectiveStatus("/elevated on", { @@ -171,7 +171,7 @@ describe("directive behavior", () => { }); expect(deniedText).toContain("agents.list[].tools.elevated.enabled"); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); it("applies per-agent allowlist requirements before allowing elevated", async () => { const { text: deniedText } = await runDirectiveStatus("/elevated on", { @@ -193,7 +193,7 @@ describe("directive behavior", () => { elevatedAllowed: true, }); expect(allowedText).toContain("Elevated mode set to ask"); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); it("handles runtime warning, invalid level, and multi-directive elevated inputs", async () => { for (const scenario of [ @@ -221,7 +221,7 @@ describe("directive behavior", () => { expect(text).toContain(snippet); } } - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); it("persists queue overrides and reset behavior", async () => { const interrupt = await runDirectiveStatus("/queue interrupt"); @@ -256,7 +256,7 @@ describe("directive behavior", () => { expect(reset.sessionEntry.queueDebounceMs).toBeUndefined(); expect(reset.sessionEntry.queueCap).toBeUndefined(); expect(reset.sessionEntry.queueDrop).toBeUndefined(); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); it("shows current trace level and persists trace directives", async () => { @@ -281,7 +281,7 @@ describe("directive behavior", () => { expect(raw.text).toContain("Trace set to raw."); expect(raw.text).toContain("may contain sensitive information"); expect(raw.sessionEntry.traceLevel).toBe("raw"); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); it("blocks /trace for non-owners without delegated gateway scope", async () => { diff --git a/src/auto-reply/reply.test-harness.ts b/src/auto-reply/reply.test-harness.ts index 149cb3173b6..28690ea2542 100644 --- a/src/auto-reply/reply.test-harness.ts +++ b/src/auto-reply/reply.test-harness.ts @@ -5,7 +5,7 @@ import { afterAll, beforeAll, vi, type Mock } from "vitest"; import { withFastReplyConfig } from "./reply/get-reply-fast-path.js"; type ReplyRuntimeMocks = { - runEmbeddedPiAgent: Mock; + runEmbeddedAgent: Mock; loadModelCatalog: Mock; webAuthExists: Mock; getWebAuthAgeMs: Mock; @@ -14,7 +14,7 @@ type ReplyRuntimeMocks = { const replyRuntimeMockState = vi.hoisted(() => ({ mocks: { - runEmbeddedPiAgent: vi.fn(), + runEmbeddedAgent: vi.fn(), loadModelCatalog: vi.fn(), webAuthExists: vi.fn().mockResolvedValue(true), getWebAuthAgeMs: vi.fn().mockReturnValue(120_000), @@ -22,14 +22,13 @@ const replyRuntimeMockState = vi.hoisted(() => ({ } as ReplyRuntimeMocks, })); -vi.mock("../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: (...args: unknown[]) => - replyRuntimeMockState.mocks.runEmbeddedPiAgent(...args), - queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), +vi.mock("../agents/embedded-agent.js", () => ({ + abortEmbeddedAgentRun: vi.fn().mockReturnValue(false), + runEmbeddedAgent: (...args: unknown[]) => replyRuntimeMockState.mocks.runEmbeddedAgent(...args), + queueEmbeddedAgentMessage: vi.fn().mockReturnValue(false), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunActive: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunStreaming: vi.fn().mockReturnValue(false), })); vi.mock("../agents/model-catalog.runtime.js", () => ({ @@ -55,13 +54,13 @@ vi.mock("../plugins/runtime/runtime-web-channel-plugin.js", () => ({ readWebSelfId: (...args: unknown[]) => replyRuntimeMockState.mocks.readWebSelfId(...args), })); -vi.mock("../agents/pi-embedded.runtime.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), +vi.mock("../agents/embedded-agent.runtime.js", () => ({ + abortEmbeddedAgentRun: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunActive: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunStreaming: vi.fn().mockReturnValue(false), resolveActiveEmbeddedRunSessionId: vi.fn().mockReturnValue(undefined), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, - waitForEmbeddedPiRunEnd: vi.fn(async () => undefined), + waitForEmbeddedAgentRunEnd: vi.fn(async () => undefined), })); vi.mock("./reply/agent-runner.runtime.js", () => ({ @@ -93,7 +92,7 @@ vi.mock("./reply/agent-runner.runtime.js", () => ({ }; }; }) => { - const result = await replyRuntimeMockState.mocks.runEmbeddedPiAgent({ + const result = await replyRuntimeMockState.mocks.runEmbeddedAgent({ prompt: params.followupRun.prompt || params.commandBody, agentDir: params.followupRun.run.agentDir, agentId: params.followupRun.run.agentId, @@ -127,7 +126,6 @@ type HomeEnvSnapshot = { HOMEPATH: string | undefined; OPENCLAW_STATE_DIR: string | undefined; OPENCLAW_AGENT_DIR: string | undefined; - PI_CODING_AGENT_DIR: string | undefined; }; function snapshotHomeEnv(): HomeEnvSnapshot { @@ -138,7 +136,6 @@ function snapshotHomeEnv(): HomeEnvSnapshot { HOMEPATH: process.env.HOMEPATH, OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR, OPENCLAW_AGENT_DIR: process.env.OPENCLAW_AGENT_DIR, - PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR, }; } @@ -175,7 +172,6 @@ export function createTempHomeHarness(options: { prefix: string; beforeEachCase? process.env.USERPROFILE = home; process.env.OPENCLAW_STATE_DIR = path.join(home, ".openclaw"); process.env.OPENCLAW_AGENT_DIR = path.join(home, ".openclaw", "agent"); - process.env.PI_CODING_AGENT_DIR = path.join(home, ".openclaw", "agent"); if (process.platform === "win32") { const match = home.match(/^([A-Za-z]:)(.*)$/); @@ -215,7 +211,7 @@ export function makeReplyConfig(home: string) { export function createReplyRuntimeMocks(): ReplyRuntimeMocks { return { - runEmbeddedPiAgent: vi.fn(), + runEmbeddedAgent: vi.fn(), loadModelCatalog: vi.fn(), webAuthExists: vi.fn().mockResolvedValue(true), getWebAuthAgeMs: vi.fn().mockReturnValue(120_000), @@ -228,7 +224,7 @@ export function installReplyRuntimeMocks(mocks: ReplyRuntimeMocks) { } export function resetReplyRuntimeMocks(mocks: ReplyRuntimeMocks) { - mocks.runEmbeddedPiAgent.mockClear(); + mocks.runEmbeddedAgent.mockClear(); mocks.loadModelCatalog.mockClear(); mocks.loadModelCatalog.mockResolvedValue([ { id: "claude-opus-4-6", name: "Opus 4.5", provider: "anthropic" }, diff --git a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts index 13a1087fcc9..026bdf37c60 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.filters-usage-summary-current-model-provider.cases.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { getProviderUsageMocks, - getRunEmbeddedPiAgentMock, + getRunEmbeddedAgentMock, makeCfg, requireSessionStorePath, withTempHome, @@ -56,7 +56,7 @@ export function registerTriggerHandlingUsageSummaryCases(params: { describe("usage and status command handling", () => { it("shows status without invoking the agent", async () => { await withTempHome(async (home) => { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); const getReplyFromConfig = getReplyFromConfigNow(params.getReplyFromConfig); seedUsageSummary(); @@ -76,13 +76,13 @@ export function registerTriggerHandlingUsageSummaryCases(params: { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toContain("Model:"); expect(text).toContain("OpenClaw"); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); }); it("cycles usage footer modes and persists the final selection", async () => { await withTempHome(async (home) => { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); const getReplyFromConfig = getReplyFromConfigNow(params.getReplyFromConfig); const cfg = makeCfg(home); cfg.session = { ...cfg.session, store: join(home, "usage-cycle.sessions.json") }; @@ -148,7 +148,7 @@ export function registerTriggerHandlingUsageSummaryCases(params: { expect((pickFirstStoreEntry(finalStore) as { responseUsage?: string })?.responseUsage).toBe( "tokens", ); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); }); }); diff --git a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts index 16288147503..509f2b7e0c5 100644 --- a/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts +++ b/src/auto-reply/reply.triggers.trigger-handling.targets-active-session-native-stop.e2e.test.ts @@ -3,13 +3,13 @@ import { join } from "node:path"; import { describe, expect, it, vi } from "vitest"; import { expectInlineCommandHandledAndStripped, - getAbortEmbeddedPiRunMock, - getCompactEmbeddedPiSessionMock, - getRunEmbeddedPiAgentMock, + getAbortEmbeddedAgentRunMock, + getCompactEmbeddedAgentSessionMock, + getRunEmbeddedAgentMock, installTriggerHandlingReplyHarness, MAIN_SESSION_KEY, makeCfg, - mockRunEmbeddedPiAgentOk, + mockRunEmbeddedAgentOk, requireSessionStorePath, expectBareNewOrResetAcknowledged, withTempHome, @@ -52,7 +52,7 @@ vi.mock("./reply/agent-runner.runtime.js", () => ({ }; }; }) => { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); const normalizeErrorText = (message: string) => { if (/context window exceeded/i.test(message)) { return "⚠️ Context overflow — prompt too large for this model. Try a shorter message or a larger-context model."; @@ -71,7 +71,7 @@ vi.mock("./reply/agent-runner.runtime.js", () => ({ }; try { - const result = await runEmbeddedPiAgentMock({ + const result = await runEmbeddedAgentMock({ prompt: params.commandBody, provider: params.followupRun.run.provider, model: params.followupRun.run.model, @@ -130,20 +130,20 @@ function formatDateStampForZone(nowMs: number, timeZone: string): string { } function mockEmbeddedOkPayload() { - return mockRunEmbeddedPiAgentOk("ok"); + return mockRunEmbeddedAgentOk("ok"); } -function mockRunEmbeddedPiAgentText(text: string, durationMs: number) { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockReset(); - runEmbeddedPiAgentMock.mockResolvedValue({ +function mockRunEmbeddedAgentText(text: string, durationMs: number) { + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); + runEmbeddedAgentMock.mockReset(); + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text }], meta: { durationMs, agentMeta: { sessionId: "s", provider: "p", model: "m" }, }, }); - return runEmbeddedPiAgentMock; + return runEmbeddedAgentMock; } async function writeDailyMemoryNotes( @@ -235,11 +235,11 @@ async function expectNextRunUsesTargetSession( params: { cfg: ReturnType; targetSessionKey: string; - runEmbeddedPiAgentMock: ReturnType; + runEmbeddedAgentMock: ReturnType; }, expected: Record, ) { - mockRunEmbeddedPiAgentText("ok", 5); + mockRunEmbeddedAgentText("ok", 5); await getReplyFromConfig( makeTelegramSessionMessage("hi", params.targetSessionKey), @@ -247,8 +247,8 @@ async function expectNextRunUsesTargetSession( params.cfg, ); - expect(params.runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - const runParams = firstMockCallArg(params.runEmbeddedPiAgentMock, "embedded PI agent"); + expect(params.runEmbeddedAgentMock).toHaveBeenCalledOnce(); + const runParams = firstMockCallArg(params.runEmbeddedAgentMock, "embedded OpenClaw agent"); for (const [key, value] of Object.entries(expected)) { expect(runParams[key]).toEqual(value); } @@ -270,7 +270,7 @@ async function writeStoredModelOverride(cfg: ReturnType): Promis } function mockSuccessfulCompaction() { - getCompactEmbeddedPiSessionMock().mockResolvedValue({ + getCompactEmbeddedAgentSessionMock().mockResolvedValue({ ok: true, compacted: true, result: { @@ -294,8 +294,8 @@ function makeUnauthorizedWhatsAppCfg(home: string) { async function expectResetBlockedForNonOwner(params: { home: string }): Promise { const { home } = params; - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockClear(); + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); + runEmbeddedAgentMock.mockClear(); const cfg = makeCfg(home); cfg.channels ??= {}; cfg.channels.whatsapp = { @@ -321,11 +321,11 @@ async function expectResetBlockedForNonOwner(params: { home: string }): Promise< cfg, ); expect(res).toBeUndefined(); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); } function mockEmbeddedOk() { - return mockRunEmbeddedPiAgentOk("ok"); + return mockRunEmbeddedAgentOk("ok"); } async function runInlineUnauthorizedCommand(params: { home: string; command: "/status" }) { @@ -364,29 +364,29 @@ describe("trigger handling", () => { ] as const) { it(`surfaces agent error: ${testCase.error}`, async () => { await withTempHome(async (home) => { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockReset(); - runEmbeddedPiAgentMock.mockImplementation(async () => { + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); + runEmbeddedAgentMock.mockReset(); + runEmbeddedAgentMock.mockImplementation(async () => { throw new Error(testCase.error); }); const errorRes = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home)); expect(maybeReplyText(errorRes), testCase.error).toBe(testCase.expected); - expect(runEmbeddedPiAgentMock, testCase.error).toHaveBeenCalledOnce(); + expect(runEmbeddedAgentMock, testCase.error).toHaveBeenCalledOnce(); }); }); } it("strips heartbeat-only replies and preserves normal text", async () => { await withTempHome(async (home) => { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); const tokenCases = [ { text: HEARTBEAT_TOKEN, expected: undefined }, { text: `${HEARTBEAT_TOKEN} hello`, expected: "hello" }, ] as const; for (const testCase of tokenCases) { - runEmbeddedPiAgentMock.mockReset(); - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockReset(); + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: testCase.text }], meta: { durationMs: 1, @@ -395,7 +395,7 @@ describe("trigger handling", () => { }); const res = await getReplyFromConfig(BASE_MESSAGE, {}, makeCfg(home)); expect(maybeReplyText(res)).toBe(testCase.expected); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); } }); }); @@ -411,14 +411,14 @@ describe("trigger handling", () => { { stamp: yesterdayStamp, text: "yesterday startup note" }, ]); - const runEmbeddedPiAgentMock = mockRunEmbeddedPiAgentText("hello", 1); + const runEmbeddedAgentMock = mockRunEmbeddedAgentText("hello", 1); const cfg = makeStartupContextCfg(home); const res = await runAuthorizedSmsCommand("/new", cfg); expect(maybeReplyText(res)).toBe("✅ New session started."); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); }); @@ -431,14 +431,14 @@ describe("trigger handling", () => { { stamp: todayStamp, text: "reset startup note" }, ]); - const runEmbeddedPiAgentMock = mockRunEmbeddedPiAgentText("hello", 1); + const runEmbeddedAgentMock = mockRunEmbeddedAgentText("hello", 1); const cfg = makeStartupContextCfg(home, { applyOn: ["reset"] }); const res = await runAuthorizedSmsCommand("/RESET", cfg); expect(maybeReplyText(res)).toBe("✅ Session reset."); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); }); @@ -474,16 +474,17 @@ describe("trigger handling", () => { ] as const; for (const testCase of thinkCases) { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockReset(); - mockRunEmbeddedPiAgentOk(); + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); + runEmbeddedAgentMock.mockReset(); + mockRunEmbeddedAgentOk(); const res = await getReplyFromConfig(testCase.request, testCase.options, makeCfg(home)); const text = maybeReplyText(res); expect(text, testCase.label).toBe("ok"); expect(text, testCase.label).not.toMatch(/Thinking level set/i); - expect(runEmbeddedPiAgentMock, testCase.label).toHaveBeenCalledOnce(); + expect(runEmbeddedAgentMock, testCase.label).toHaveBeenCalledOnce(); if (testCase.assertPrompt) { - const prompt = firstMockCallArg(runEmbeddedPiAgentMock, "embedded PI agent").prompt ?? ""; + const prompt = + firstMockCallArg(runEmbeddedAgentMock, "embedded OpenClaw agent").prompt ?? ""; expect(prompt).toContain("Give me the status"); expect(prompt).not.toContain("/thinking high"); expect(prompt).not.toContain("/think high"); @@ -516,8 +517,8 @@ describe("trigger handling", () => { ] as const; for (const testCase of modelCases) { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockReset(); + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); + runEmbeddedAgentMock.mockReset(); mockEmbeddedOkPayload(); const cfg = makeCfg(home); cfg.session = { ...cfg.session, store: join(home, `${testCase.label}.sessions.json`) }; @@ -525,7 +526,7 @@ describe("trigger handling", () => { testCase.setup(cfg); await getReplyFromConfig(BASE_MESSAGE, { isHeartbeat: true }, cfg); - const call = firstMockCallArg(runEmbeddedPiAgentMock, "embedded PI agent"); + const call = firstMockCallArg(runEmbeddedAgentMock, "embedded OpenClaw agent"); expect(call?.provider).toBe(testCase.expected.provider); expect(call?.model).toBe(testCase.expected.model); } @@ -555,7 +556,7 @@ describe("trigger handling", () => { ); const text = maybeReplyText(res); expect(text?.startsWith("⚙️ Compacted")).toBe(true); - expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce(); + expect(getCompactEmbeddedAgentSessionMock()).toHaveBeenCalledOnce(); const store = loadSessionStore(storePath); const sessionKey = resolveSessionKey("per-sender", request); expect(store[sessionKey]?.compactionCount).toBe(1); @@ -564,7 +565,7 @@ describe("trigger handling", () => { it("compacts worker sessions via the agent session file", async () => { await withTempHome(async (home) => { - getCompactEmbeddedPiSessionMock().mockReset(); + getCompactEmbeddedAgentSessionMock().mockReset(); mockSuccessfulCompaction(); const cfg = makeCfg(home); cfg.session = { ...cfg.session, store: join(home, "compact-worker.sessions.json") }; @@ -582,9 +583,10 @@ describe("trigger handling", () => { const text = maybeReplyText(res); expect(text?.startsWith("⚙️ Compacted")).toBe(true); - expect(getCompactEmbeddedPiSessionMock()).toHaveBeenCalledOnce(); + expect(getCompactEmbeddedAgentSessionMock()).toHaveBeenCalledOnce(); expect( - firstMockCallArg(getCompactEmbeddedPiSessionMock(), "embedded PI compaction").sessionFile, + firstMockCallArg(getCompactEmbeddedAgentSessionMock(), "embedded OpenClaw compaction") + .sessionFile, ).toContain(join("agents", "worker1", "sessions")); }); }); @@ -593,7 +595,7 @@ describe("trigger handling", () => { await withTempHome(async (home) => { const cfg = makeCfg(home); cfg.session = { ...cfg.session, store: join(home, "native-stop.sessions.json") }; - getAbortEmbeddedPiRunMock().mockReset().mockReturnValue(false); + getAbortEmbeddedAgentRunMock().mockReset().mockReturnValue(false); const storePath = cfg.session?.store; if (!storePath) { throw new Error("missing session store path"); @@ -655,7 +657,7 @@ describe("trigger handling", () => { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("⚙️ Agent was aborted."); - expect(getAbortEmbeddedPiRunMock()).toHaveBeenCalledWith(targetSessionId); + expect(getAbortEmbeddedAgentRunMock()).toHaveBeenCalledWith(targetSessionId); const store = loadSessionStore(storePath); expect(store[targetSessionKey]?.abortedLastRun).toBe(true); expect(getFollowupQueueDepth(targetSessionKey)).toBe(0); @@ -666,8 +668,8 @@ describe("trigger handling", () => { await withTempHome(async (home) => { const cfg = makeCfg(home); cfg.session = { ...cfg.session, store: join(home, "native-model.sessions.json") }; - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockReset(); + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); + runEmbeddedAgentMock.mockReset(); const storePath = requireSessionStorePath(cfg); const slashSessionKey = "telegram:slash:111"; const targetSessionKey = MAIN_SESSION_KEY; @@ -692,7 +694,7 @@ describe("trigger handling", () => { expect(store[slashSessionKey]).toBeUndefined(); await expectNextRunUsesTargetSession( - { cfg, targetSessionKey, runEmbeddedPiAgentMock }, + { cfg, targetSessionKey, runEmbeddedAgentMock }, { provider: "openai", model: "gpt-4.1-mini", @@ -715,8 +717,8 @@ describe("trigger handling", () => { }, }; cfg.session = { ...cfg.session, store: join(home, "native-model-thread.sessions.json") }; - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockReset(); + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); + runEmbeddedAgentMock.mockReset(); const storePath = requireSessionStorePath(cfg); const slashSessionKey = "agent:main:telegram:slash:7595562691"; const targetSessionKey = "agent:main:main:thread:7595562691:12812"; @@ -751,7 +753,7 @@ describe("trigger handling", () => { expect(store[slashSessionKey]).toBeUndefined(); await expectNextRunUsesTargetSession( - { cfg, targetSessionKey, runEmbeddedPiAgentMock }, + { cfg, targetSessionKey, runEmbeddedAgentMock }, { provider: "deepseek", model: "deepseek-v4-pro", @@ -764,8 +766,8 @@ describe("trigger handling", () => { await withTempHome(async (home) => { const cfg = makeCfg(home); cfg.session = { ...cfg.session, store: join(home, "native-model-auth.sessions.json") }; - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockReset(); + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); + runEmbeddedAgentMock.mockReset(); const storePath = requireSessionStorePath(cfg); const authDir = join(home, ".openclaw", "agents", "main", "agent"); await fs.mkdir(authDir, { recursive: true }); @@ -828,7 +830,7 @@ describe("trigger handling", () => { expect(store[slashSessionKey]).toBeUndefined(); await expectNextRunUsesTargetSession( - { cfg, targetSessionKey, runEmbeddedPiAgentMock }, + { cfg, targetSessionKey, runEmbeddedAgentMock }, { provider: "openai-codex", model: "gpt-5.4", @@ -851,15 +853,15 @@ describe("trigger handling", () => { blockReplyContains: "Identity", requestOverrides: { SenderId: "12345" }, }); - const inlineRunEmbeddedPiAgentMock = mockEmbeddedOk(); + const inlineRunEmbeddedAgentMock = mockEmbeddedOk(); const res = await runInlineUnauthorizedCommand({ home, command: "/status", }); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe("ok"); - expect(inlineRunEmbeddedPiAgentMock).toHaveBeenCalled(); - const prompt = inlineRunEmbeddedPiAgentMock.mock.calls.at(-1)?.[0]?.prompt ?? ""; + expect(inlineRunEmbeddedAgentMock).toHaveBeenCalled(); + const prompt = inlineRunEmbeddedAgentMock.mock.calls.at(-1)?.[0]?.prompt ?? ""; expect(prompt).toContain("/status"); }); }); diff --git a/src/auto-reply/reply/abort.test.ts b/src/auto-reply/reply/abort.test.ts index f34b00cde73..2c50b9ce85a 100644 --- a/src/auto-reply/reply/abort.test.ts +++ b/src/auto-reply/reply/abort.test.ts @@ -28,8 +28,8 @@ import { } from "./reply-run-registry.js"; import { buildTestCtx } from "./test-ctx.js"; -vi.mock("../../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(true), +vi.mock("../../agents/embedded-agent.js", () => ({ + abortEmbeddedAgentRun: vi.fn().mockReturnValue(true), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, })); @@ -71,7 +71,7 @@ const acpManagerMocks = vi.hoisted(() => ({ })); const runtimeAbortMocks = vi.hoisted(() => ({ - abortEmbeddedPiRun: vi.fn(() => true), + abortEmbeddedAgentRun: vi.fn(() => true), resolveActiveEmbeddedRunSessionId: vi.fn(() => undefined as string | undefined), })); @@ -193,7 +193,7 @@ describe("abort detection", () => { resolveSession: acpManagerMocks.resolveSession, cancelSession: acpManagerMocks.cancelSession, }) as never) as never, - abortEmbeddedPiRun: runtimeAbortMocks.abortEmbeddedPiRun, + abortEmbeddedAgentRun: runtimeAbortMocks.abortEmbeddedAgentRun, resolveActiveEmbeddedRunSessionId: runtimeAbortMocks.resolveActiveEmbeddedRunSessionId, getLatestSubagentRunByChildSessionKey: subagentRegistryMocks.getLatestSubagentRunByChildSessionKey, @@ -216,7 +216,7 @@ describe("abort detection", () => { commandQueueMocks.clearCommandLane.mockClear().mockReturnValue(1); acpManagerMocks.resolveSession.mockReset().mockReturnValue({ kind: "none" }); acpManagerMocks.cancelSession.mockReset().mockResolvedValue(undefined); - runtimeAbortMocks.abortEmbeddedPiRun.mockReset().mockReturnValue(true); + runtimeAbortMocks.abortEmbeddedAgentRun.mockReset().mockReturnValue(true); runtimeAbortMocks.resolveActiveEmbeddedRunSessionId.mockReset().mockReturnValue(undefined); subagentRegistryMocks.getLatestSubagentRunByChildSessionKey.mockReset().mockReturnValue(null); }); @@ -398,12 +398,12 @@ describe("abort detection", () => { const storeKey = "agent:main:telegram:group:-1001234567890:topic:99"; const lookupKey = "Agent:Main:Telegram:Group:-1001234567890:Topic:99"; const store = { - [storeKey]: { sessionId: "pi-topic-99", updatedAt: 0 }, + [storeKey]: { sessionId: "agent-topic-99", updatedAt: 0 }, } as Record; // Direct lookup fails (store uses lowercase keys); normalization fallback must succeed. expect(store[lookupKey]).toBeUndefined(); const result = resolveSessionEntryForKey(store, lookupKey); - expect(result.entry?.sessionId).toBe("pi-topic-99"); + expect(result.entry?.sessionId).toBe("agent-topic-99"); expect(result.key).toBe(storeKey); }); @@ -446,7 +446,7 @@ describe("abort detection", () => { expect(result.handled).toBe(true); expect(runtimeAbortMocks.resolveActiveEmbeddedRunSessionId).toHaveBeenCalledWith(sessionKey); - expect(runtimeAbortMocks.abortEmbeddedPiRun).toHaveBeenCalledWith(activeSessionId); + expect(runtimeAbortMocks.abortEmbeddedAgentRun).toHaveBeenCalledWith(activeSessionId); expect(getFollowupQueueDepth(sessionKey)).toBe(0); expectSessionLaneCleared(sessionKey); }); @@ -641,7 +641,7 @@ describe("abort detection", () => { }); expect(result.handled).toBe(true); - expect(runtimeAbortMocks.abortEmbeddedPiRun).toHaveBeenCalledWith("source-store-session"); + expect(runtimeAbortMocks.abortEmbeddedAgentRun).toHaveBeenCalledWith("source-store-session"); expect(getFollowupQueueDepth(sourceSessionKey)).toBe(0); expect(getFollowupQueueDepth(acpSessionKey)).toBe(0); expectSessionLaneCleared(sourceSessionKey); diff --git a/src/auto-reply/reply/abort.ts b/src/auto-reply/reply/abort.ts index 493eca8db47..38814477a6e 100644 --- a/src/auto-reply/reply/abort.ts +++ b/src/auto-reply/reply/abort.ts @@ -1,9 +1,9 @@ import { getAcpSessionManager } from "../../acp/control-plane/manager.js"; import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { - abortEmbeddedPiRun, + abortEmbeddedAgentRun, resolveActiveEmbeddedRunSessionId, -} from "../../agents/pi-embedded-runner/runs.js"; +} from "../../agents/embedded-agent-runner/runs.js"; import { getLatestSubagentRunByChildSessionKey, listSubagentRunsForController, @@ -62,7 +62,7 @@ export { const defaultAbortDeps = { getAcpSessionManager, - abortEmbeddedPiRun, + abortEmbeddedAgentRun, resolveActiveEmbeddedRunSessionId, getLatestSubagentRunByChildSessionKey, listSubagentRunsForController, @@ -77,7 +77,8 @@ export const testing = { setDepsForTests(deps: Partial | undefined): void { abortDeps.getAcpSessionManager = deps?.getAcpSessionManager ?? defaultAbortDeps.getAcpSessionManager; - abortDeps.abortEmbeddedPiRun = deps?.abortEmbeddedPiRun ?? defaultAbortDeps.abortEmbeddedPiRun; + abortDeps.abortEmbeddedAgentRun = + deps?.abortEmbeddedAgentRun ?? defaultAbortDeps.abortEmbeddedAgentRun; abortDeps.resolveActiveEmbeddedRunSessionId = deps?.resolveActiveEmbeddedRunSessionId ?? defaultAbortDeps.resolveActiveEmbeddedRunSessionId; abortDeps.getLatestSubagentRunByChildSessionKey = @@ -90,7 +91,7 @@ export const testing = { }, resetDepsForTests(): void { abortDeps.getAcpSessionManager = defaultAbortDeps.getAcpSessionManager; - abortDeps.abortEmbeddedPiRun = defaultAbortDeps.abortEmbeddedPiRun; + abortDeps.abortEmbeddedAgentRun = defaultAbortDeps.abortEmbeddedAgentRun; abortDeps.resolveActiveEmbeddedRunSessionId = defaultAbortDeps.resolveActiveEmbeddedRunSessionId; abortDeps.getLatestSubagentRunByChildSessionKey = @@ -116,7 +117,7 @@ export function abortSessionRunTarget(params: { key?: string; sessionId?: string let aborted = key ? replyRunRegistry.abort(key) : false; for (const sessionId of sessionIds) { - aborted = abortDeps.abortEmbeddedPiRun(sessionId) || aborted; + aborted = abortDeps.abortEmbeddedAgentRun(sessionId) || aborted; } return aborted; } diff --git a/src/auto-reply/reply/acp-projector.ts b/src/auto-reply/reply/acp-projector.ts index 87f6f6fb3b2..8de6488e78d 100644 --- a/src/auto-reply/reply/acp-projector.ts +++ b/src/auto-reply/reply/acp-projector.ts @@ -1,5 +1,5 @@ import type { AcpRuntimeEvent, AcpSessionUpdateTag } from "../../acp/runtime/types.js"; -import { EmbeddedBlockChunker } from "../../agents/pi-embedded-block-chunker.js"; +import { EmbeddedBlockChunker } from "../../agents/embedded-agent-block-chunker.js"; import { formatToolSummary, resolveToolDisplay } from "../../agents/tool-display.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { prefixSystemMessage } from "../../infra/system-message.js"; diff --git a/src/auto-reply/reply/agent-runner-cli-dispatch.ts b/src/auto-reply/reply/agent-runner-cli-dispatch.ts index eb13041d513..31a244eea85 100644 --- a/src/auto-reply/reply/agent-runner-cli-dispatch.ts +++ b/src/auto-reply/reply/agent-runner-cli-dispatch.ts @@ -1,6 +1,6 @@ import { runCliAgent } from "../../agents/cli-runner.js"; import type { RunCliAgentParams } from "../../agents/cli-runner/types.js"; -import type { EmbeddedPiRunResult } from "../../agents/pi-embedded.js"; +import type { EmbeddedAgentRunResult } from "../../agents/embedded-agent.js"; import { emitAgentEvent, onAgentEvent } from "../../infra/agent-events.js"; import { normalizeLowercaseStringOrEmpty, @@ -66,8 +66,8 @@ export async function runCliAgentWithLifecycle(params: { onAssistantText?: (text: string) => Promise; onReasoningText?: (text: string) => Promise; onErrorBeforeLifecycle?: (err: unknown) => Promise; - transformResult?: (result: EmbeddedPiRunResult) => EmbeddedPiRunResult; -}): Promise { + transformResult?: (result: EmbeddedAgentRunResult) => EmbeddedAgentRunResult; +}): Promise { const startedAt = params.startedAt ?? Date.now(); const emitLifecycleStart = params.emitLifecycleStart ?? true; const emitLifecycleTerminal = params.emitLifecycleTerminal ?? true; diff --git a/src/auto-reply/reply/agent-runner-execution.test.ts b/src/auto-reply/reply/agent-runner-execution.test.ts index 91db9072bf6..218a9bee520 100644 --- a/src/auto-reply/reply/agent-runner-execution.test.ts +++ b/src/auto-reply/reply/agent-runner-execution.test.ts @@ -15,6 +15,7 @@ import { buildContextOverflowRecoveryText, computeContextAwareReserveTokensFloor, MAX_LIVE_SWITCH_RETRIES, + resolveSessionRuntimeOverrideForProvider, resolveRunAfterAutoFallbackPrimaryProbeRecheck, } from "./agent-runner-execution.js"; import { HEARTBEAT_EXTERNAL_RUN_FAILURE_TEXT } from "./agent-runner-failure-copy.js"; @@ -24,7 +25,7 @@ import type { ReplyOperation } from "./reply-run-registry.js"; import type { TypingSignaler } from "./typing-mode.js"; const state = vi.hoisted(() => ({ - runEmbeddedPiAgentMock: vi.fn(), + runEmbeddedAgentMock: vi.fn(), runCliAgentMock: vi.fn(), runWithModelFallbackMock: vi.fn(), isCliProviderMock: vi.fn((_: unknown) => false), @@ -39,6 +40,17 @@ const state = vi.hoisted(() => ({ const GENERIC_RUN_FAILURE_TEXT = "⚠️ Something went wrong while processing your request. Please try again, or use /new to start a fresh session."; +describe("resolveSessionRuntimeOverrideForProvider", () => { + it("ignores unsupported session runtime pins", () => { + expect( + resolveSessionRuntimeOverrideForProvider({ + provider: "openai", + entry: { agentRuntimeOverride: "unsupported-runtime" }, + }), + ).toBeUndefined(); + }); +}); + function makeTestModel(id: string, contextTokens: number): ModelDefinitionConfig { return { id, @@ -52,8 +64,8 @@ function makeTestModel(id: string, contextTokens: number): ModelDefinitionConfig }; } -vi.mock("../../agents/pi-embedded.js", () => ({ - runEmbeddedPiAgent: (params: unknown) => state.runEmbeddedPiAgentMock(params), +vi.mock("../../agents/embedded-agent.js", () => ({ + runEmbeddedAgent: (params: unknown) => state.runEmbeddedAgentMock(params), })); vi.mock("../../agents/cli-runner.js", () => ({ @@ -82,7 +94,7 @@ vi.mock("../../agents/bootstrap-budget.js", () => ({ resolveBootstrapWarningSignaturesSeen: () => [], })); -vi.mock("../../agents/pi-embedded-helpers.js", () => ({ +vi.mock("../../agents/embedded-agent-helpers.js", () => ({ BILLING_ERROR_USER_MESSAGE: "billing", formatRateLimitOrOverloadedErrorCopy: (message: string) => { if (/model\s+(?:is\s+)?at capacity/i.test(message)) { @@ -1089,7 +1101,7 @@ describe("buildContextOverflowRecoveryText", () => { describe("runAgentTurnWithFallback", () => { beforeEach(() => { - state.runEmbeddedPiAgentMock.mockReset(); + state.runEmbeddedAgentMock.mockReset(); state.runCliAgentMock.mockReset(); state.runWithModelFallbackMock.mockReset(); state.isCliProviderMock.mockReset(); @@ -1262,7 +1274,7 @@ describe("runAgentTurnWithFallback", () => { model: params.model, attempts: [], })); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "user model" }], meta: { agentMeta: { @@ -1360,7 +1372,7 @@ describe("runAgentTurnWithFallback", () => { model: "gemini-3-pro", attempts: [{ provider: "anthropic", model: "claude-sonnet-4-6", error: "rate limit" }], })); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "fallback" }], meta: {}, }); @@ -1368,7 +1380,7 @@ describe("runAgentTurnWithFallback", () => { const runAgentTurnWithFallback = await getRunAgentTurnWithFallback(); await runAgentTurnWithFallback(createMinimalRunAgentTurnParams({ followupRun })); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 0, "embedded run", { + expectMockCallArgFields(state.runEmbeddedAgentMock, 0, "embedded run", { provider: "google", model: "gemini-3-pro", authProfileId: "google:fallback", @@ -1413,7 +1425,7 @@ describe("runAgentTurnWithFallback", () => { { provider: "openai", model: "gpt-5.4", error: "rate limit" }, ], })); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "fallback" }], meta: {}, }); @@ -1426,7 +1438,7 @@ describe("runAgentTurnWithFallback", () => { getActiveSessionEntry: () => activeSessionStore[sessionKey], }); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 0, "embedded run", { + expectMockCallArgFields(state.runEmbeddedAgentMock, 0, "embedded run", { provider: "openai", model: "gpt-5.5", authProfileId: "openai:fallback", @@ -1481,7 +1493,7 @@ describe("runAgentTurnWithFallback", () => { attempts: [{ provider: "anthropic", model: "claude-sonnet-4-6", error: "rate limit" }], }; }); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "fallback" }], meta: {}, }); @@ -1543,7 +1555,7 @@ describe("runAgentTurnWithFallback", () => { attempts: [{ provider: "openai", model: "gpt-5.5", error: "rate limit" }], }; }); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "fallback" }], meta: {}, }); @@ -1591,20 +1603,20 @@ describe("runAgentTurnWithFallback", () => { attempts: [{ provider: "openai", model: "gpt-5.5", error: "rate limit" }], }; }); - state.runEmbeddedPiAgentMock + state.runEmbeddedAgentMock .mockResolvedValueOnce({ payloads: [], meta: {} }) .mockResolvedValueOnce({ payloads: [{ text: "fallback" }], meta: {} }); const runAgentTurnWithFallback = await getRunAgentTurnWithFallback(); await runAgentTurnWithFallback(createMinimalRunAgentTurnParams({ followupRun })); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 0, "primary run", { + expectMockCallArgFields(state.runEmbeddedAgentMock, 0, "primary run", { provider: "openai", model: "gpt-5.5", authProfileId: "openai:primary", authProfileIdSource: "auto", }); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 1, "fallback run", { + expectMockCallArgFields(state.runEmbeddedAgentMock, 1, "fallback run", { provider: "openai", model: "gpt-5.4", authProfileId: "openai:fallback", @@ -1651,7 +1663,7 @@ describe("runAgentTurnWithFallback", () => { attempts: [], }; }); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "primary recovered" }], meta: { agentMeta: { @@ -1713,7 +1725,7 @@ describe("runAgentTurnWithFallback", () => { attempts: [], }; }); - state.runEmbeddedPiAgentMock + state.runEmbeddedAgentMock .mockImplementationOnce(async () => { throw new LiveSessionModelSwitchError({ provider: "openai", @@ -1742,7 +1754,7 @@ describe("runAgentTurnWithFallback", () => { expect(result.kind).toBe("success"); expect(attemptedProviders).toEqual(["anthropic", "openai"]); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 1, "embedded run", { + expectMockCallArgFields(state.runEmbeddedAgentMock, 1, "embedded run", { provider: "openai", model: "gpt-5.4", authProfileId: "openai:primary", @@ -2290,7 +2302,7 @@ describe("runAgentTurnWithFallback", () => { model: "claude-sonnet-4-7", attempts: [], })); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { const realAgentEvents = await vi.importActual( "../../infra/agent-events.js", ); @@ -2394,7 +2406,7 @@ describe("runAgentTurnWithFallback", () => { model: "gpt-5.4", attempts: [], })); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "fallback" }], meta: {}, }); @@ -2423,10 +2435,10 @@ describe("runAgentTurnWithFallback", () => { expect(result.kind).toBe("success"); expect(state.runCliAgentMock).not.toHaveBeenCalled(); - expect(state.runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); + expect(state.runEmbeddedAgentMock).toHaveBeenCalledOnce(); expect( requireRecord( - requireMockCall(state.runEmbeddedPiAgentMock, 0, "embedded run params")[0], + requireMockCall(state.runEmbeddedAgentMock, 0, "embedded run params")[0], "embedded run params", ), ).not.toHaveProperty("agentHarnessId", "claude-cli"); @@ -2439,7 +2451,7 @@ describe("runAgentTurnWithFallback", () => { model: "gpt-5.4", attempts: [], })); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "openai" }], meta: {}, }); @@ -2455,19 +2467,19 @@ describe("runAgentTurnWithFallback", () => { ({ sessionId: "session", updatedAt: Date.now(), - agentRuntimeOverride: "pi", + agentRuntimeOverride: "openclaw", }) as SessionEntry, }); expect(result.kind).toBe("success"); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 0, "embedded run params", { + expectMockCallArgFields(state.runEmbeddedAgentMock, 0, "embedded run params", { provider: "openai", model: "gpt-5.4", - agentHarnessId: "pi", + agentHarnessId: "openclaw", }); }); - it("honors Pi session runtime overrides before CLI runtime aliases", async () => { + it("honors agent session runtime overrides before CLI runtime aliases", async () => { state.isCliProviderMock.mockImplementation((provider: unknown) => provider === "claude-cli"); state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => ({ result: await params.run("anthropic", "claude-opus-4-7"), @@ -2475,8 +2487,8 @@ describe("runAgentTurnWithFallback", () => { model: "claude-opus-4-7", attempts: [], })); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ - payloads: [{ text: "pi" }], + state.runEmbeddedAgentMock.mockResolvedValueOnce({ + payloads: [{ text: "agent" }], meta: {}, }); @@ -2498,22 +2510,22 @@ describe("runAgentTurnWithFallback", () => { ({ sessionId: "session", updatedAt: Date.now(), - agentRuntimeOverride: "pi", + agentRuntimeOverride: "openclaw", }) as SessionEntry, }); expect(result.kind).toBe("success"); expect(state.runCliAgentMock).not.toHaveBeenCalled(); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 0, "embedded run params", { + expectMockCallArgFields(state.runEmbeddedAgentMock, 0, "embedded run params", { provider: "anthropic", model: "claude-opus-4-7", - agentHarnessId: "pi", + agentHarnessId: "openclaw", }); }); it("forwards media-only tool results without typing text", async () => { const onToolResult = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onToolResult?.({ mediaUrls: ["/tmp/generated.png"] }); return { payloads: [{ text: "final" }], meta: {} }; }); @@ -2563,7 +2575,7 @@ describe("runAgentTurnWithFallback", () => { }); it("surfaces model capacity errors from no-text mid-turn failures", async () => { - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "thinking", isReasoning: true }], meta: { error: { @@ -2653,7 +2665,7 @@ describe("runAgentTurnWithFallback", () => { const followupRun = createFollowupRun(); followupRun.run.provider = "openai-codex"; followupRun.run.model = "gpt-5.4"; - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [ { text: "agent stopped after repeated plan-only turns without taking a concrete action.", @@ -2738,7 +2750,7 @@ describe("runAgentTurnWithFallback", () => { params.directlySentBlockKeys?.add("block:1"); }, ); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onBlockReply?.({ text: "streamed block" }); return { payloads: [], meta: {} }; }); @@ -2857,7 +2869,7 @@ describe("runAgentTurnWithFallback", () => { compactionCount: 0, }; const activeSessionStore = { main: sessionEntry }; - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ payloads: [], meta: {} }); + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {} }); state.runWithModelFallbackMock.mockImplementationOnce(async (params: FallbackRunnerParams) => { const failedResult = await params.run("openai-codex", "gpt-5.4"); expect(sessionEntry.providerOverride).toBe("openai-codex"); @@ -2896,7 +2908,7 @@ describe("runAgentTurnWithFallback", () => { it("strips a glued leading NO_REPLY token from streamed tool results", async () => { const onToolResult = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onToolResult?.({ text: "NO_REPLYThe user is saying hello" }); return { payloads: [{ text: "final" }], meta: {} }; }); @@ -2944,7 +2956,7 @@ describe("runAgentTurnWithFallback", () => { } delivered.push(payload.text ?? ""); }); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { void params.onToolResult?.({ text: "first", mediaUrls: [] }); void params.onToolResult?.({ text: "second", mediaUrls: [] }); return { payloads: [{ text: "final" }], meta: {} }; @@ -2989,7 +3001,7 @@ describe("runAgentTurnWithFallback", () => { await new Promise((resolve) => setTimeout(resolve, delay)); deliveryOrder.push(payload.text ?? ""); }); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { void params.onToolResult?.({ text: "first", mediaUrls: [] }); void params.onToolResult?.({ text: "second", mediaUrls: [] }); return { payloads: [{ text: "final" }], meta: {} }; @@ -3029,7 +3041,7 @@ describe("runAgentTurnWithFallback", () => { it("forwards item lifecycle events to reply options", async () => { const onItemEvent = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "item", data: { @@ -3088,7 +3100,7 @@ describe("runAgentTurnWithFallback", () => { it("skips channel item progress when a matching tool event carries the progress", async () => { const onItemEvent = vi.fn(); const onToolStart = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "item", data: { @@ -3136,7 +3148,7 @@ describe("runAgentTurnWithFallback", () => { it("preserves suppressed item progress when no tool-start callback is registered", async () => { const onItemEvent = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "item", data: { @@ -3184,7 +3196,7 @@ describe("runAgentTurnWithFallback", () => { it("forwards raw tool progress detail mode to tool-start reply options", async () => { const onToolStart = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "tool", data: { @@ -3225,7 +3237,7 @@ describe("runAgentTurnWithFallback", () => { releaseTyping = resolve; }), ); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { void params.onAgentEvent?.({ stream: "tool", data: { @@ -3266,7 +3278,7 @@ describe("runAgentTurnWithFallback", () => { it("leaves Codex app-server telemetry publication to the harness", async () => { const agentEvents = await import("../../infra/agent-events.js"); const emitAgentEvent = vi.mocked(agentEvents.emitAgentEvent); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "codex_app_server.guardian", sessionKey: "agent:main:subagent:codex-child", @@ -3312,7 +3324,7 @@ describe("runAgentTurnWithFallback", () => { it("emits an embedded lifecycle terminal backstop when the runner returns without one", async () => { const agentEvents = await import("../../infra/agent-events.js"); const emitAgentEvent = vi.mocked(agentEvents.emitAgentEvent); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "lifecycle", data: { phase: "start", startedAt: 1_000 }, @@ -3375,7 +3387,7 @@ describe("runAgentTurnWithFallback", () => { it("does not duplicate embedded lifecycle terminal events already reported by the runner", async () => { const agentEvents = await import("../../infra/agent-events.js"); const emitAgentEvent = vi.mocked(agentEvents.emitAgentEvent); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "lifecycle", data: { phase: "start", startedAt: 1_000 }, @@ -3425,7 +3437,7 @@ describe("runAgentTurnWithFallback", () => { model: "gpt-5.4", attempts: [], })); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({ + state.runEmbeddedAgentMock.mockImplementationOnce(async () => ({ payloads: [ { text: [ @@ -3495,7 +3507,7 @@ describe("runAgentTurnWithFallback", () => { "Third, code fences and richer structured outputs are left untouched so technical answers stay intact.", "Finally, the overlay reinforces that this is a live chat and nudges the model toward short natural replies.", ].join(" "); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({ + state.runEmbeddedAgentMock.mockImplementationOnce(async () => ({ payloads: [{ text: longDetailedReply }], meta: {}, })); @@ -3538,7 +3550,7 @@ describe("runAgentTurnWithFallback", () => { const onApprovalEvent = vi.fn(); const onCommandOutput = vi.fn(); const onPatchSummary = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "plan", data: { @@ -3675,7 +3687,7 @@ describe("runAgentTurnWithFallback", () => { await itemEventGate; }); const onCommandOutput = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "tool", data: { @@ -3761,7 +3773,7 @@ describe("runAgentTurnWithFallback", () => { it("keeps progress callbacks active after message-tool-only reads", async () => { const onItemEvent = vi.fn(); const onCommandOutput = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "tool", data: { @@ -3848,7 +3860,7 @@ describe("runAgentTurnWithFallback", () => { it("keeps compaction start notices silent by default", async () => { const onBlockReply = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } }); return { payloads: [{ text: "final" }], meta: {} }; }); @@ -3885,7 +3897,7 @@ describe("runAgentTurnWithFallback", () => { const onBlockReply = vi.fn(); const onCompactionStart = vi.fn(); const onCompactionEnd = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } }); await params.onAgentEvent?.({ stream: "compaction", @@ -3930,7 +3942,7 @@ describe("runAgentTurnWithFallback", () => { it("emits a compaction start notice when notifyUser is enabled", async () => { const onBlockReply = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } }); return { payloads: [{ text: "final" }], meta: {} }; }); @@ -3982,7 +3994,7 @@ describe("runAgentTurnWithFallback", () => { it("emits a compaction completion notice when notifyUser is enabled", async () => { const onBlockReply = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } }); await params.onAgentEvent?.({ stream: "compaction", @@ -4043,7 +4055,7 @@ describe("runAgentTurnWithFallback", () => { it("delivers compaction hook messages without duplicating notifyUser notices", async () => { const onBlockReply = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "compaction", data: { phase: "start", messages: ["Hook before"] }, @@ -4109,7 +4121,7 @@ describe("runAgentTurnWithFallback", () => { it("prefers onCompactionEnd callback over default notice when notifyUser is enabled", async () => { const onBlockReply = vi.fn(); const onCompactionEnd = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } }); await params.onAgentEvent?.({ stream: "compaction", @@ -4166,7 +4178,7 @@ describe("runAgentTurnWithFallback", () => { it("emits an incomplete compaction notice when compaction ends without completing", async () => { const onBlockReply = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { await params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } }); await params.onAgentEvent?.({ stream: "compaction", @@ -4569,7 +4581,7 @@ describe("runAgentTurnWithFallback", () => { }); it("uses compact generic copy for raw external chat errors when verbose is off", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("INVALID_ARGUMENT: some other failure"), ); @@ -4604,7 +4616,7 @@ describe("runAgentTurnWithFallback", () => { }); it("uses heartbeat failure copy for raw external errors during heartbeat runs", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error('Command lane "main" task timed out after 120000ms'), ); @@ -4702,7 +4714,7 @@ describe("runAgentTurnWithFallback", () => { ); it("forwards sanitized generic errors on external chat channels when verbose is on", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("INVALID_ARGUMENT: some other failure"), ); @@ -4741,7 +4753,7 @@ describe("runAgentTurnWithFallback", () => { it.each(["group", "channel"] as const)( "keeps raw runner failure boilerplate out of Discord %s chats", async (chatType) => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("openai-codex/gpt-5.5 ended with an incomplete terminal response"), ); @@ -4769,7 +4781,7 @@ describe("runAgentTurnWithFallback", () => { it.each(["group", "channel"] as const)( "surfaces raw runner failure copy in Discord %s chats when silentReply.group is set to disallow", async (chatType) => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("openai-codex/gpt-5.5 ended with an incomplete terminal response"), ); @@ -4806,7 +4818,7 @@ describe("runAgentTurnWithFallback", () => { ); it("surfaces raw runner failure copy when per-surface silentReply.group is set to disallow", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("openai-codex/gpt-5.5 ended with an incomplete terminal response"), ); @@ -4851,7 +4863,7 @@ describe("runAgentTurnWithFallback", () => { // Sanity check: explicit `{}` config (no silentReply) must still resolve // to the documented default `group: "allow"` and produce a silent payload // — the new policy hookup must not regress the default behavior. - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("openai-codex/gpt-5.5 ended with an incomplete terminal response"), ); @@ -4970,7 +4982,7 @@ describe("runAgentTurnWithFallback", () => { }); it("uses compact generic copy for raw runner failures in normal Discord direct chats", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("openai-codex/gpt-5.5 ended with an incomplete terminal response"), ); @@ -4993,7 +5005,7 @@ describe("runAgentTurnWithFallback", () => { }); it("keeps raw runner failure guidance visible in verbose Discord direct chats", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("openai-codex/gpt-5.5 ended with an incomplete terminal response"), ); @@ -5018,7 +5030,7 @@ describe("runAgentTurnWithFallback", () => { }); it("formats raw Codex API payloads before forwarding verbose external errors", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error( 'Codex error: {"type":"error","error":{"type":"server_error","message":"Something exploded"},"sequence_number":2}', ), @@ -5058,7 +5070,7 @@ describe("runAgentTurnWithFallback", () => { it("preserves the active session when embedded overflow recovery fails", async () => { state.isContextOverflowErrorMock.mockReturnValue(true); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: { error: { @@ -5106,7 +5118,7 @@ describe("runAgentTurnWithFallback", () => { it("preserves the active session when compaction failure is thrown before reply", async () => { state.isCompactionFailureErrorMock.mockReturnValue(true); - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("Auto-compaction failed: nothing to compact"), ); @@ -5181,7 +5193,7 @@ describe("runAgentTurnWithFallback", () => { }); it("surfaces gateway reauth guidance for known OAuth refresh failures", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error( "OAuth token refresh failed for openai-codex: refresh_token_reused. Please try again or re-authenticate.", ), @@ -5220,7 +5232,7 @@ describe("runAgentTurnWithFallback", () => { }); it("surfaces direct provider auth guidance for missing API keys", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error( 'No API key found for provider "openai". You are authenticated with OpenAI Codex OAuth; OpenAI agent model runs use openai/gpt-* through the Codex runtime. Set OPENAI_API_KEY only for direct OpenAI API-key surfaces. | No API key found for provider "openai". You are authenticated with OpenAI Codex OAuth; OpenAI agent model runs use openai/gpt-* through the Codex runtime. Set OPENAI_API_KEY only for direct OpenAI API-key surfaces.', ), @@ -5259,7 +5271,7 @@ describe("runAgentTurnWithFallback", () => { }); it("falls back to a generic provider message for unsafe missing-key provider ids", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error('No API key found for provider "openai`\nrm -rf /".'), ); @@ -5296,7 +5308,7 @@ describe("runAgentTurnWithFallback", () => { }); it("falls back to a generic reauth command when the provider in the OAuth error is unsafe", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error( "OAuth token refresh failed for openai-codex`\nrm -rf /: invalid_grant. Please try again or re-authenticate.", ), @@ -5335,7 +5347,7 @@ describe("runAgentTurnWithFallback", () => { }); it("returns a session reset hint for Bedrock tool mismatch errors on external chat channels", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error( "The number of toolResult blocks at messages.186.content exceeds the number of toolUse blocks of previous turn.", ), @@ -5372,7 +5384,7 @@ describe("runAgentTurnWithFallback", () => { }); it("returns a provider conversation-state error for OpenAI missing custom tool output errors on external chat channels", async () => { - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("Custom tool call output is missing for call id: call_live_123."), ); @@ -5408,7 +5420,7 @@ describe("runAgentTurnWithFallback", () => { it("does not auto-reset role-ordering provider conversation-state errors", async () => { const resetSessionAfterRoleOrderingConflict = vi.fn(async () => true); - state.runEmbeddedPiAgentMock.mockRejectedValueOnce(new Error("400 Incorrect role information")); + state.runEmbeddedAgentMock.mockRejectedValueOnce(new Error("400 Incorrect role information")); const runAgentTurnWithFallback = await getRunAgentTurnWithFallback(); const result = await runAgentTurnWithFallback({ @@ -5443,7 +5455,7 @@ describe("runAgentTurnWithFallback", () => { it("keeps raw generic errors on internal control surfaces", async () => { state.isInternalMessageChannelMock.mockReturnValue(true); - state.runEmbeddedPiAgentMock.mockRejectedValueOnce( + state.runEmbeddedAgentMock.mockRejectedValueOnce( new Error("INVALID_ARGUMENT: some other failure"), ); @@ -5493,7 +5505,7 @@ describe("runAgentTurnWithFallback", () => { attempts: [], }), ); - state.runEmbeddedPiAgentMock + state.runEmbeddedAgentMock .mockImplementationOnce(async () => { throw new LiveSessionModelSwitchError({ provider: "openai", @@ -5539,7 +5551,7 @@ describe("runAgentTurnWithFallback", () => { }); expect(result.kind).toBe("success"); - expect(state.runEmbeddedPiAgentMock).toHaveBeenCalledTimes(2); + expect(state.runEmbeddedAgentMock).toHaveBeenCalledTimes(2); expect(followupRun.run.provider).toBe("openai"); expect(followupRun.run.model).toBe("gpt-5.4"); }); @@ -5560,7 +5572,7 @@ describe("runAgentTurnWithFallback", () => { }; }, ); - state.runEmbeddedPiAgentMock.mockImplementation(async () => { + state.runEmbeddedAgentMock.mockImplementation(async () => { throw new LiveSessionModelSwitchError({ provider: "openai", model: "gpt-5.4", @@ -5621,7 +5633,7 @@ describe("runAgentTurnWithFallback", () => { }; }, ); - state.runEmbeddedPiAgentMock + state.runEmbeddedAgentMock .mockImplementationOnce(async () => { throw new LiveSessionModelSwitchError({ provider: "openai", @@ -5678,7 +5690,7 @@ describe("runAgentTurnWithFallback", () => { // Two switches (within the limit of 2) then success on third attempt expect(result.kind).toBe("success"); - expect(state.runEmbeddedPiAgentMock).toHaveBeenCalledTimes(3); + expect(state.runEmbeddedAgentMock).toHaveBeenCalledTimes(3); expect(followupRun.run.provider).toBe("openai"); expect(followupRun.run.model).toBe("gpt-5.4"); expect(followupRun.run.authProfileId).toBe("profile-c"); @@ -5701,7 +5713,7 @@ describe("runAgentTurnWithFallback", () => { authProfileOverrideSource: "user", }; const sessionStore = { main: sessionEntry }; - state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => { + state.runEmbeddedAgentMock.mockImplementationOnce(async () => { sessionEntry.providerOverride = "zai"; sessionEntry.modelOverride = "glm-5"; sessionEntry.authProfileOverride = "zai:work"; @@ -5752,7 +5764,7 @@ describe("runAgentTurnWithFallback", () => { attempts: [], }), ); - state.runEmbeddedPiAgentMock.mockResolvedValue({ + state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: {}, }); @@ -5797,8 +5809,8 @@ describe("runAgentTurnWithFallback", () => { }); expect(result.kind).toBe("success"); - expect(state.runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 0, "embedded run params", { + expect(state.runEmbeddedAgentMock).toHaveBeenCalledTimes(1); + expectMockCallArgFields(state.runEmbeddedAgentMock, 0, "embedded run params", { provider: "openai-codex", model: "gpt-5.4", authProfileId: undefined, @@ -5826,7 +5838,7 @@ describe("runAgentTurnWithFallback", () => { attempts: [], }), ); - state.runEmbeddedPiAgentMock.mockResolvedValue({ + state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: {}, }); @@ -5888,7 +5900,7 @@ describe("runAgentTurnWithFallback", () => { attempts: [], }), ); - state.runEmbeddedPiAgentMock.mockResolvedValue({ + state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: {}, }); @@ -5955,7 +5967,7 @@ describe("runAgentTurnWithFallback", () => { attempts: [], }), ); - state.runEmbeddedPiAgentMock.mockResolvedValue({ + state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: {}, }); @@ -6090,7 +6102,7 @@ describe("runAgentTurnWithFallback", () => { attempts: [], }; }); - state.runEmbeddedPiAgentMock.mockImplementationOnce( + state.runEmbeddedAgentMock.mockImplementationOnce( async (args: { onAssistantErrorMessagePersisted?: (message: { role: "assistant"; @@ -6106,8 +6118,8 @@ describe("runAgentTurnWithFallback", () => { throw new Error("upstream 500"); }, ); - state.runEmbeddedPiAgentMock.mockRejectedValueOnce(new Error("upstream 500")); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockRejectedValueOnce(new Error("upstream 500")); + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: {}, }); @@ -6115,14 +6127,14 @@ describe("runAgentTurnWithFallback", () => { const runAgentTurnWithFallback = await getRunAgentTurnWithFallback(); await runAgentTurnWithFallback(createMinimalRunAgentTurnParams()); - expect(state.runEmbeddedPiAgentMock).toHaveBeenCalledTimes(3); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 0, "primary candidate", { + expect(state.runEmbeddedAgentMock).toHaveBeenCalledTimes(3); + expectMockCallArgFields(state.runEmbeddedAgentMock, 0, "primary candidate", { suppressAssistantErrorPersistence: false, }); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 1, "first fallback candidate", { + expectMockCallArgFields(state.runEmbeddedAgentMock, 1, "first fallback candidate", { suppressAssistantErrorPersistence: true, }); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 2, "second fallback candidate", { + expectMockCallArgFields(state.runEmbeddedAgentMock, 2, "second fallback candidate", { suppressAssistantErrorPersistence: true, }); }); @@ -6139,7 +6151,7 @@ describe("runAgentTurnWithFallback", () => { }; }); state.runCliAgentMock.mockRejectedValueOnce(new Error("cli failed")); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: {}, }); @@ -6148,8 +6160,8 @@ describe("runAgentTurnWithFallback", () => { await runAgentTurnWithFallback(createMinimalRunAgentTurnParams()); expect(state.runCliAgentMock).toHaveBeenCalledOnce(); - expect(state.runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 0, "embedded fallback candidate", { + expect(state.runEmbeddedAgentMock).toHaveBeenCalledOnce(); + expectMockCallArgFields(state.runEmbeddedAgentMock, 0, "embedded fallback candidate", { suppressAssistantErrorPersistence: false, }); }); @@ -6164,7 +6176,7 @@ describe("runAgentTurnWithFallback", () => { attempts: [], }; }); - state.runEmbeddedPiAgentMock.mockImplementationOnce( + state.runEmbeddedAgentMock.mockImplementationOnce( async (args: { onUserMessagePersisted?: (m: { role: "user"; @@ -6178,7 +6190,7 @@ describe("runAgentTurnWithFallback", () => { throw new Error("upstream 500"); }, ); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: {}, }); @@ -6186,11 +6198,11 @@ describe("runAgentTurnWithFallback", () => { const runAgentTurnWithFallback = await getRunAgentTurnWithFallback(); await runAgentTurnWithFallback(createMinimalRunAgentTurnParams()); - expect(state.runEmbeddedPiAgentMock).toHaveBeenCalledTimes(2); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 0, "primary candidate", { + expect(state.runEmbeddedAgentMock).toHaveBeenCalledTimes(2); + expectMockCallArgFields(state.runEmbeddedAgentMock, 0, "primary candidate", { suppressNextUserMessagePersistence: false, }); - expectMockCallArgFields(state.runEmbeddedPiAgentMock, 1, "fallback candidate", { + expectMockCallArgFields(state.runEmbeddedAgentMock, 1, "fallback candidate", { suppressNextUserMessagePersistence: true, }); }); diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index db5ab9b1c24..e9841726a14 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -30,7 +30,7 @@ import { resolveModelRefFromString, resolvePersistedOverrideModelRef, } from "../../agents/model-selection.js"; -import { resolveOpenAIRuntimeProviderForPi } from "../../agents/openai-codex-routing.js"; +import { resolveOpenAIRuntimeProvider } from "../../agents/openai-codex-routing.js"; import { BILLING_ERROR_USER_MESSAGE, formatRateLimitOrOverloadedErrorCopy, @@ -41,10 +41,10 @@ import { isOverloadedErrorMessage, isRateLimitErrorMessage, isTransientHttpError, -} from "../../agents/pi-embedded-helpers.js"; -import { sanitizeUserFacingText } from "../../agents/pi-embedded-helpers/sanitize-user-facing-text.js"; -import { isMessagingToolSendAction } from "../../agents/pi-embedded-messaging.js"; -import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; +} from "../../agents/embedded-agent-helpers.js"; +import { sanitizeUserFacingText } from "../../agents/embedded-agent-helpers/sanitize-user-facing-text.js"; +import { isMessagingToolSendAction } from "../../agents/embedded-agent-messaging.js"; +import { runEmbeddedAgent } from "../../agents/embedded-agent.js"; import { buildAgentRuntimeOutcomePlan } from "../../agents/runtime-plan/build.js"; import { resolveGroupSessionKey, @@ -279,7 +279,7 @@ export type AgentRunLoopResult = | { kind: "success"; runId: string; - runResult: Awaited>; + runResult: Awaited>; fallbackProvider?: string; fallbackModel?: string; fallbackAttempts: RuntimeFallbackAttempt[]; @@ -290,7 +290,7 @@ export type AgentRunLoopResult = } | { kind: "final"; payload: ReplyPayload }; -type EmbeddedAgentRunResult = Awaited>; +type EmbeddedAgentRunResult = Awaited>; type FallbackSelectionState = Pick< SessionEntry, @@ -1302,9 +1302,6 @@ export function resolveSessionRuntimeOverrideForProvider(params: { if (!runtime || runtime === "auto" || runtime === "default") { return undefined; } - if (runtime === "pi") { - return "pi"; - } if (provider === "openai" && runtime === "codex") { return "codex"; } @@ -1592,7 +1589,7 @@ export async function runAgentTurnWithFallback(params: { isControlUiVisible: shouldSurfaceToControlUi, }); } - let runResult: Awaited>; + let runResult: Awaited>; let fallbackProvider = params.followupRun.run.provider; let fallbackModel = params.followupRun.run.model; let attemptedRuntimeProvider = fallbackProvider; @@ -1986,20 +1983,18 @@ export async function runAgentTurnWithFallback(params: { config: runtimeConfig, }); const resolvedCliExecutionProvider = - resolvedSessionRuntimeOverride === "pi" - ? provider - : ((resolvedSessionRuntimeOverride && - isCliProvider(resolvedSessionRuntimeOverride, runtimeConfig) - ? resolvedSessionRuntimeOverride - : undefined) ?? - resolveCliRuntimeExecutionProvider({ - provider, - cfg: runtimeConfig, - agentId: params.followupRun.run.agentId, - modelId: model, - authProfileId: resolvedSelectedAuthProfile.authProfileId, - }) ?? - provider); + (resolvedSessionRuntimeOverride && + isCliProvider(resolvedSessionRuntimeOverride, runtimeConfig) + ? resolvedSessionRuntimeOverride + : undefined) ?? + resolveCliRuntimeExecutionProvider({ + provider, + cfg: runtimeConfig, + agentId: params.followupRun.run.agentId, + modelId: model, + authProfileId: resolvedSelectedAuthProfile.authProfileId, + }) ?? + provider; return { sessionRuntimeOverride: resolvedSessionRuntimeOverride, cliExecutionProvider: resolvedCliExecutionProvider, @@ -2137,7 +2132,7 @@ export async function runAgentTurnWithFallback(params: { agentId: params.followupRun.run.agentId, sessionKey: params.followupRun.run.runtimePolicySessionKey ?? params.sessionKey, }); - const embeddedRunProvider = resolveOpenAIRuntimeProviderForPi({ + const embeddedRunProvider = resolveOpenAIRuntimeProvider({ provider, harnessRuntime: agentHarnessPolicy.runtime, authProfileProvider: runBaseParams.authProfileId?.split(":", 1)[0], @@ -2147,8 +2142,8 @@ export async function runAgentTurnWithFallback(params: { }); const embeddedRunHarnessOverride = sessionRuntimeOverride ?? - (agentHarnessPolicy.runtime === "pi" && embeddedRunProvider !== provider - ? "pi" + (agentHarnessPolicy.runtime === "openclaw" && embeddedRunProvider !== provider + ? "openclaw" : undefined); return (async () => { let attemptCompactionCount = 0; @@ -2166,7 +2161,7 @@ export async function runAgentTurnWithFallback(params: { milestone: "before_embedded_run", }); const result = await agentTurnTiming.measure("embedded_run", () => - runEmbeddedPiAgent({ + runEmbeddedAgent({ ...embeddedContext, allowGatewaySubagentBinding: true, trigger: params.isHeartbeat ? "heartbeat" : "user", diff --git a/src/auto-reply/reply/agent-runner-memory.dedup.test.ts b/src/auto-reply/reply/agent-runner-memory.dedup.test.ts index fc77b8a0938..0cd05802fe5 100644 --- a/src/auto-reply/reply/agent-runner-memory.dedup.test.ts +++ b/src/auto-reply/reply/agent-runner-memory.dedup.test.ts @@ -11,7 +11,7 @@ import crypto from "node:crypto"; import { describe, expect, it } from "vitest"; // Inline computeContextHash to avoid importing memory-flush.js (which -// triggers the full agent import chain and hits the missing pi-ai/oauth +// triggers the full agent import chain and hits the missing shared model runtime/oauth // package in test environments). This mirrors the implementation in // src/auto-reply/reply/memory-flush.ts exactly. function computeContextHash(messages: Array<{ role?: string; content?: unknown }>): string { diff --git a/src/auto-reply/reply/agent-runner-memory.test.ts b/src/auto-reply/reply/agent-runner-memory.test.ts index 0de78a0ffa5..fb361269e19 100644 --- a/src/auto-reply/reply/agent-runner-memory.test.ts +++ b/src/auto-reply/reply/agent-runner-memory.test.ts @@ -17,9 +17,9 @@ import { import { createTestFollowupRun, writeTestSessionStore } from "./agent-runner.test-fixtures.js"; import type { ReplyOperation } from "./reply-run-registry.js"; -const compactEmbeddedPiSessionMock = vi.fn(); +const compactEmbeddedAgentSessionMock = vi.fn(); const runWithModelFallbackMock = vi.fn(); -const runEmbeddedPiAgentMock = vi.fn(); +const runEmbeddedAgentMock = vi.fn(); const refreshQueuedFollowupSessionMock = vi.fn(); const incrementCompactionCountMock = vi.fn(); const ensureSelectedAgentHarnessPluginMock = vi.fn(); @@ -78,7 +78,7 @@ type ModelFallbackParams = { }) => Promise | void; }; -type EmbeddedPiAgentParams = { +type EmbeddedAgentParams = { provider?: string; model?: string; authProfileId?: unknown; @@ -93,7 +93,7 @@ type EmbeddedPiAgentParams = { abortSignal?: AbortSignal; }; -type CompactEmbeddedPiSessionParams = { +type CompactEmbeddedAgentSessionParams = { agentId?: string; authProfileId?: string; contextTokenBudget?: number; @@ -123,20 +123,20 @@ function requireModelFallbackCall(index = 0) { return call; } -function requireEmbeddedPiAgentCall(index = 0) { - const call = runEmbeddedPiAgentMock.mock.calls[index]?.[0] as EmbeddedPiAgentParams | undefined; +function requireEmbeddedAgentCall(index = 0) { + const call = runEmbeddedAgentMock.mock.calls[index]?.[0] as EmbeddedAgentParams | undefined; if (!call) { - throw new Error(`runEmbeddedPiAgent call ${index} missing`); + throw new Error(`runEmbeddedAgent call ${index} missing`); } return call; } -function requireCompactEmbeddedPiSessionCall(index = 0) { - const call = compactEmbeddedPiSessionMock.mock.calls[index]?.[0] as - | CompactEmbeddedPiSessionParams +function requireCompactEmbeddedAgentSessionCall(index = 0) { + const call = compactEmbeddedAgentSessionMock.mock.calls[index]?.[0] as + | CompactEmbeddedAgentSessionParams | undefined; if (!call) { - throw new Error(`compactEmbeddedPiSession call ${index} missing`); + throw new Error(`compactEmbeddedAgentSession call ${index} missing`); } return call; } @@ -160,12 +160,12 @@ describe("runMemoryFlushIfNeeded", () => { model, attempts: [], })); - compactEmbeddedPiSessionMock.mockReset().mockResolvedValue({ + compactEmbeddedAgentSessionMock.mockReset().mockResolvedValue({ ok: true, compacted: true, result: { tokensAfter: 42 }, }); - runEmbeddedPiAgentMock.mockReset().mockResolvedValue({ payloads: [], meta: {} }); + runEmbeddedAgentMock.mockReset().mockResolvedValue({ payloads: [], meta: {} }); refreshQueuedFollowupSessionMock.mockReset(); ensureMemoryFlushTargetFileMock.mockReset().mockResolvedValue(undefined); ensureSelectedAgentHarnessPluginMock.mockReset().mockResolvedValue(undefined); @@ -198,9 +198,9 @@ describe("runMemoryFlushIfNeeded", () => { return nextEntry.compactionCount; }); setAgentRunnerMemoryTestDeps({ - compactEmbeddedPiSession: compactEmbeddedPiSessionMock as never, + compactEmbeddedAgentSession: compactEmbeddedAgentSessionMock as never, runWithModelFallback: runWithModelFallbackMock as never, - runEmbeddedPiAgent: runEmbeddedPiAgentMock as never, + runEmbeddedAgent: runEmbeddedAgentMock as never, ensureMemoryFlushTargetFile: ensureMemoryFlushTargetFileMock as never, refreshQueuedFollowupSession: refreshQueuedFollowupSessionMock as never, incrementCompactionCount: incrementCompactionCountMock as never, @@ -229,7 +229,7 @@ describe("runMemoryFlushIfNeeded", () => { const sessionStore = { [sessionKey]: sessionEntry }; await writeTestSessionStore(storePath, sessionKey, sessionEntry); - runEmbeddedPiAgentMock.mockImplementationOnce( + runEmbeddedAgentMock.mockImplementationOnce( async (params: { onAgentEvent?: (evt: { stream: string; data: { phase: string } }) => void; }) => { @@ -267,8 +267,8 @@ describe("runMemoryFlushIfNeeded", () => { expect(entry?.sessionId).toBe("session-rotated"); expect(followupRun.run.sessionId).toBe("session-rotated"); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); - const flushCall = requireEmbeddedPiAgentCall(); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); + const flushCall = requireEmbeddedAgentCall(); expect(flushCall.prompt).toContain("Pre-compaction memory flush."); expect(flushCall.transcriptPrompt).toBe(""); expect(flushCall.prompt).not.toBe(flushCall.transcriptPrompt); @@ -279,7 +279,7 @@ describe("runMemoryFlushIfNeeded", () => { relativePath: flushCall.memoryFlushWritePath, }); expect(ensureMemoryFlushTargetFileMock.mock.invocationCallOrder[0]).toBeLessThan( - runEmbeddedPiAgentMock.mock.invocationCallOrder[0] ?? 0, + runEmbeddedAgentMock.mock.invocationCallOrder[0] ?? 0, ); expect(refreshQueuedFollowupSessionMock).toHaveBeenCalledTimes(1); const refreshCall = requireRefreshQueuedFollowupSessionCall(); @@ -305,7 +305,7 @@ describe("runMemoryFlushIfNeeded", () => { compactionCount: 1, }; const visibleErrorPayloads: Array<{ text?: string; isError?: boolean }> = []; - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [ { text: "normal silent maintenance reply" }, { @@ -539,8 +539,8 @@ describe("runMemoryFlushIfNeeded", () => { expect(fallbackCall.model).toBe("qwen3:8b"); expect(fallbackCall.abortSignal).toBe(replyOperation.abortSignal); expect(fallbackCall.fallbacksOverride).toEqual([]); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); - const agentCall = requireEmbeddedPiAgentCall(); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); + const agentCall = requireEmbeddedAgentCall(); expect(agentCall.provider).toBe("ollama"); expect(agentCall.model).toBe("qwen3:8b"); expect(agentCall.abortSignal).toBe(replyOperation.abortSignal); @@ -611,6 +611,37 @@ describe("runMemoryFlushIfNeeded", () => { }); }); + it("ignores stale runtime pins before memory-flush fallback preflight", async () => { + const sessionEntry: SessionEntry = { + sessionId: "session", + updatedAt: Date.now(), + totalTokens: 80_000, + compactionCount: 1, + agentRuntimeOverride: "unsupported-runtime", + }; + + await runMemoryFlushIfNeeded({ + cfg: { agents: { defaults: { compaction: { memoryFlush: {} } } } }, + followupRun: createTestFollowupRun({ + provider: "openai", + model: "gpt-5.4", + }), + sessionCtx: { Provider: "telegram" } as unknown as TemplateContext, + defaultModel: "openai/gpt-5.4", + agentCfgContextTokens: 100_000, + resolvedVerboseLevel: "off", + sessionEntry, + sessionStore: { main: sessionEntry }, + sessionKey: "main", + isHeartbeat: false, + replyOperation: createReplyOperation(), + }); + + expect( + requireModelFallbackCall().resolveAgentHarnessRuntimeOverride?.("openai", "gpt-5.4"), + ).toBeUndefined(); + }); + it("skips memory flush for CLI providers", async () => { const sessionEntry: SessionEntry = { sessionId: "session", @@ -634,7 +665,7 @@ describe("runMemoryFlushIfNeeded", () => { }); expect(entry).toBe(sessionEntry); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); it("uses runtime policy session key when checking memory-flush sandbox writability", async () => { @@ -677,7 +708,7 @@ describe("runMemoryFlushIfNeeded", () => { }); expect(entry).toBe(sessionEntry); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); it("continues when preflight compaction reports the session is already under target", async () => { @@ -695,7 +726,7 @@ describe("runMemoryFlushIfNeeded", () => { systemPrompt: "Write memory to memory/YYYY-MM-DD.md.", relativePath: "memory/2023-11-14.md", })); - compactEmbeddedPiSessionMock.mockResolvedValueOnce({ + compactEmbeddedAgentSessionMock.mockResolvedValueOnce({ ok: true, compacted: false, reason: "already under target", @@ -726,8 +757,8 @@ describe("runMemoryFlushIfNeeded", () => { }); expect(entry).toBe(sessionEntry); - expect(compactEmbeddedPiSessionMock).toHaveBeenCalledTimes(1); - expect(requireCompactEmbeddedPiSessionCall()).toMatchObject({ + expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1); + expect(requireCompactEmbeddedAgentSessionCall()).toMatchObject({ trigger: "budget", deferOwningContextEngineCompaction: false, contextTokenBudget: 100, @@ -750,7 +781,7 @@ describe("runMemoryFlushIfNeeded", () => { systemPrompt: "Write memory to memory/YYYY-MM-DD.md.", relativePath: "memory/2023-11-14.md", })); - compactEmbeddedPiSessionMock.mockResolvedValueOnce({ + compactEmbeddedAgentSessionMock.mockResolvedValueOnce({ ok: true, compacted: false, reason: "deferred to background context-engine maintenance", @@ -784,7 +815,7 @@ describe("runMemoryFlushIfNeeded", () => { "Preflight compaction required but failed: deferred to background context-engine maintenance", ); - expect(compactEmbeddedPiSessionMock).toHaveBeenCalledTimes(1); + expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1); expect(incrementCompactionCountMock).not.toHaveBeenCalled(); }); @@ -830,8 +861,8 @@ describe("runMemoryFlushIfNeeded", () => { replyOperation: createReplyOperation(), }); - expect(compactEmbeddedPiSessionMock).toHaveBeenCalledTimes(1); - const compactCall = requireCompactEmbeddedPiSessionCall(); + expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1); + const compactCall = requireCompactEmbeddedAgentSessionCall(); expect(compactCall.sessionKey).toBe("agent:main:main"); expect(compactCall.sandboxSessionKey).toBe("agent:main:telegram:default:direct:12345"); }); @@ -856,7 +887,7 @@ describe("runMemoryFlushIfNeeded", () => { systemPrompt: "Write memory to memory/YYYY-MM-DD.md.", relativePath: "memory/2023-11-14.md", })); - compactEmbeddedPiSessionMock.mockResolvedValueOnce({ + compactEmbeddedAgentSessionMock.mockResolvedValueOnce({ ok: false, compacted: false, reason, @@ -889,7 +920,7 @@ describe("runMemoryFlushIfNeeded", () => { }); expect(entry).toBe(sessionEntry); - expect(compactEmbeddedPiSessionMock).toHaveBeenCalledTimes(1); + expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1); expect(incrementCompactionCountMock).not.toHaveBeenCalled(); }, ); @@ -909,7 +940,7 @@ describe("runMemoryFlushIfNeeded", () => { systemPrompt: "Write memory to memory/YYYY-MM-DD.md.", relativePath: "memory/2023-11-14.md", })); - compactEmbeddedPiSessionMock.mockResolvedValueOnce({ + compactEmbeddedAgentSessionMock.mockResolvedValueOnce({ ok: false, compacted: false, reason: "auth profile mismatch", @@ -943,7 +974,7 @@ describe("runMemoryFlushIfNeeded", () => { }), ).rejects.toThrow("Preflight compaction required but failed: auth profile mismatch"); - expect(compactEmbeddedPiSessionMock).toHaveBeenCalledTimes(1); + expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1); expect(incrementCompactionCountMock).not.toHaveBeenCalled(); }); @@ -973,11 +1004,10 @@ describe("runMemoryFlushIfNeeded", () => { replyOperation: createReplyOperation(), }); - const compactCall = requireCompactEmbeddedPiSessionCall(); + const compactCall = requireCompactEmbeddedAgentSessionCall(); expect(compactCall.authProfileId).toBe("anthropic:claude@martian.engineering"); expect(compactCall.contextTokenBudget).toBe(258_000); }); - it("updates the active preflight run after transcript rotation", async () => { const sessionFile = path.join(rootDir, "session.jsonl"); const successorFile = path.join(rootDir, "session-rotated.jsonl"); @@ -994,7 +1024,7 @@ describe("runMemoryFlushIfNeeded", () => { systemPrompt: "Write memory to memory/YYYY-MM-DD.md.", relativePath: "memory/2023-11-14.md", })); - compactEmbeddedPiSessionMock.mockResolvedValueOnce({ + compactEmbeddedAgentSessionMock.mockResolvedValueOnce({ ok: true, compacted: true, result: { @@ -1088,12 +1118,12 @@ describe("runMemoryFlushIfNeeded", () => { replyOperation: createReplyOperation(), }); - const compactCall = requireCompactEmbeddedPiSessionCall(); + const compactCall = requireCompactEmbeddedAgentSessionCall(); expect(compactCall.currentTokenCount).toBeGreaterThanOrEqual(100_000); }); it("continues when preflight compaction returns a successful no-op", async () => { - compactEmbeddedPiSessionMock.mockResolvedValueOnce({ + compactEmbeddedAgentSessionMock.mockResolvedValueOnce({ ok: true, compacted: false, reason: "plugin already stored this turn", @@ -1125,8 +1155,8 @@ describe("runMemoryFlushIfNeeded", () => { }); expect(entry).toBe(sessionEntry); - expect(compactEmbeddedPiSessionMock).toHaveBeenCalledTimes(1); - const compactCall = requireCompactEmbeddedPiSessionCall(); + expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1); + const compactCall = requireCompactEmbeddedAgentSessionCall(); expect(compactCall.contextTokenBudget).toBe(200_000); expect(replyOperation.setPhase).toHaveBeenCalledWith("preflight_compacting"); expect(replyOperation.updateSessionId).not.toHaveBeenCalled(); @@ -1134,7 +1164,7 @@ describe("runMemoryFlushIfNeeded", () => { expect(refreshQueuedFollowupSessionMock).not.toHaveBeenCalled(); }); - it("skips OpenClaw preflight compaction for persisted Codex runtime sessions", async () => { + it("uses the persisted Codex runtime context window for OpenAI preflight compaction", async () => { registerMemoryFlushPlanResolverForTest(() => ({ softThresholdTokens: 4_000, forceFlushTranscriptBytes: 1_000_000_000, @@ -1151,7 +1181,7 @@ describe("runMemoryFlushIfNeeded", () => { agentHarnessId: "codex", }; - const entry = await runPreflightCompactionIfNeeded({ + await runPreflightCompactionIfNeeded({ cfg: { models: { providers: { @@ -1176,11 +1206,12 @@ describe("runMemoryFlushIfNeeded", () => { replyOperation: createReplyOperation(), }); - expect(entry).toBe(sessionEntry); - expect(compactEmbeddedPiSessionMock).not.toHaveBeenCalled(); + expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1); + const compactCall = requireCompactEmbeddedAgentSessionCall(); + expect(compactCall.currentTokenCount).toBe(347_000); }); - it("leaves fresh over-threshold Codex token snapshots to native Codex auto-compaction", async () => { + it("still compacts when a fresh persisted token total is over the threshold", async () => { registerMemoryFlushPlanResolverForTest(() => ({ softThresholdTokens: 4_000, forceFlushTranscriptBytes: 1_000_000_000, @@ -1197,7 +1228,7 @@ describe("runMemoryFlushIfNeeded", () => { agentHarnessId: "codex", }; - const entry = await runPreflightCompactionIfNeeded({ + await runPreflightCompactionIfNeeded({ cfg: { models: { providers: { @@ -1222,11 +1253,12 @@ describe("runMemoryFlushIfNeeded", () => { replyOperation: createReplyOperation(), }); - expect(entry).toBe(sessionEntry); - expect(compactEmbeddedPiSessionMock).not.toHaveBeenCalled(); + expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1); + const compactCall = requireCompactEmbeddedAgentSessionCall(); + expect(compactCall.currentTokenCount).toBe(347_000); }); - it("leaves policy-resolved OpenAI Codex sessions to native Codex auto-compaction", async () => { + it("keeps the OpenAI API context window for persisted OpenClaw runtime overrides", async () => { registerMemoryFlushPlanResolverForTest(() => ({ softThresholdTokens: 4_000, forceFlushTranscriptBytes: 1_000_000_000, @@ -1240,54 +1272,7 @@ describe("runMemoryFlushIfNeeded", () => { updatedAt: Date.now(), totalTokens: 347_000, totalTokensFresh: false, - }; - - const entry = await runPreflightCompactionIfNeeded({ - cfg: { - models: { - providers: { - openai: { models: [{ id: "gpt-5.5", contextWindow: 1_000_000 }] }, - "openai-codex": { models: [{ id: "gpt-5.5", contextWindow: 350_000 }] }, - }, - }, - agents: { defaults: { compaction: { memoryFlush: {} } } }, - } as never, - followupRun: createTestFollowupRun({ - provider: "openai", - model: "gpt-5.5", - sessionId: "session", - sessionKey: "agent:main:telegram:default:direct:12345", - runtimePolicySessionKey: "agent:main:telegram:default:direct:12345", - }), - defaultModel: "gpt-5.5", - sessionEntry, - sessionStore: { "agent:main:telegram:default:direct:12345": sessionEntry }, - sessionKey: "agent:main:telegram:default:direct:12345", - runtimePolicySessionKey: "agent:main:telegram:default:direct:12345", - storePath: path.join(rootDir, "sessions.json"), - isHeartbeat: false, - replyOperation: createReplyOperation(), - }); - - expect(entry).toBe(sessionEntry); - expect(compactEmbeddedPiSessionMock).not.toHaveBeenCalled(); - }); - - it("keeps the OpenAI API context window for persisted PI runtime overrides", async () => { - registerMemoryFlushPlanResolverForTest(() => ({ - softThresholdTokens: 4_000, - forceFlushTranscriptBytes: 1_000_000_000, - reserveTokensFloor: 0, - prompt: "Pre-compaction memory flush.\nNO_REPLY", - systemPrompt: "Write memory to memory/YYYY-MM-DD.md.", - relativePath: "memory/2023-11-14.md", - })); - const sessionEntry: SessionEntry = { - sessionId: "session", - updatedAt: Date.now(), - totalTokens: 347_000, - totalTokensFresh: false, - agentRuntimeOverride: "pi", + agentRuntimeOverride: "openclaw", }; const entry = await runPreflightCompactionIfNeeded({ @@ -1316,165 +1301,7 @@ describe("runMemoryFlushIfNeeded", () => { }); expect(entry).toBe(sessionEntry); - expect(compactEmbeddedPiSessionMock).not.toHaveBeenCalled(); - }); - - it("defers OpenAI preflight compaction until the configured server threshold", async () => { - registerMemoryFlushPlanResolverForTest(() => ({ - softThresholdTokens: 4_000, - forceFlushTranscriptBytes: 1_000_000_000, - reserveTokensFloor: 60_000, - prompt: "Pre-compaction memory flush.\nNO_REPLY", - systemPrompt: "Write memory to memory/YYYY-MM-DD.md.", - relativePath: "memory/2023-11-14.md", - })); - const sessionEntry: SessionEntry = { - sessionId: "session", - updatedAt: Date.now(), - totalTokens: 161_077, - totalTokensFresh: true, - agentRuntimeOverride: "pi", - }; - - const entry = await runPreflightCompactionIfNeeded({ - cfg: { - agents: { - defaults: { - models: { - "openai/gpt-5.5": { - params: { - responsesCompactThreshold: 200_000, - }, - }, - }, - compaction: { memoryFlush: {} }, - }, - }, - } as never, - followupRun: createTestFollowupRun({ - provider: "openai", - model: "gpt-5.5", - sessionId: "session", - sessionKey: "main", - }), - defaultModel: "gpt-5.5", - agentCfgContextTokens: 220_000, - sessionEntry, - sessionStore: { main: sessionEntry }, - sessionKey: "main", - storePath: path.join(rootDir, "sessions.json"), - isHeartbeat: false, - replyOperation: createReplyOperation(), - }); - - expect(entry).toBe(sessionEntry); - expect(compactEmbeddedPiSessionMock).not.toHaveBeenCalled(); - }); - - it("runs OpenAI preflight compaction at the configured server threshold", async () => { - registerMemoryFlushPlanResolverForTest(() => ({ - softThresholdTokens: 4_000, - forceFlushTranscriptBytes: 1_000_000_000, - reserveTokensFloor: 60_000, - prompt: "Pre-compaction memory flush.\nNO_REPLY", - systemPrompt: "Write memory to memory/YYYY-MM-DD.md.", - relativePath: "memory/2023-11-14.md", - })); - const sessionEntry: SessionEntry = { - sessionId: "session", - updatedAt: Date.now(), - totalTokens: 201_000, - totalTokensFresh: true, - agentRuntimeOverride: "pi", - }; - - await runPreflightCompactionIfNeeded({ - cfg: { - agents: { - defaults: { - models: { - "openai/gpt-5.5": { - params: { - responsesServerCompaction: true, - responsesCompactThreshold: 200_000, - }, - }, - }, - compaction: { memoryFlush: {} }, - }, - }, - } as never, - followupRun: createTestFollowupRun({ - provider: "openai", - model: "gpt-5.5", - sessionId: "session", - sessionKey: "main", - }), - defaultModel: "gpt-5.5", - agentCfgContextTokens: 220_000, - sessionEntry, - sessionStore: { main: sessionEntry }, - sessionKey: "main", - storePath: path.join(rootDir, "sessions.json"), - isHeartbeat: false, - replyOperation: createReplyOperation(), - }); - - const compactCall = requireCompactEmbeddedPiSessionCall(); - expect(compactCall.currentTokenCount).toBe(201_000); - }); - - it("uses the local preflight threshold when OpenAI server compaction is explicitly disabled", async () => { - registerMemoryFlushPlanResolverForTest(() => ({ - softThresholdTokens: 4_000, - forceFlushTranscriptBytes: 1_000_000_000, - reserveTokensFloor: 60_000, - prompt: "Pre-compaction memory flush.\nNO_REPLY", - systemPrompt: "Write memory to memory/YYYY-MM-DD.md.", - relativePath: "memory/2023-11-14.md", - })); - const sessionEntry: SessionEntry = { - sessionId: "session", - updatedAt: Date.now(), - totalTokens: 161_077, - totalTokensFresh: true, - agentRuntimeOverride: "pi", - }; - - await runPreflightCompactionIfNeeded({ - cfg: { - agents: { - defaults: { - models: { - "openai/gpt-5.5": { - params: { - responsesServerCompaction: false, - responsesCompactThreshold: 200_000, - }, - }, - }, - compaction: { memoryFlush: {} }, - }, - }, - } as never, - followupRun: createTestFollowupRun({ - provider: "openai", - model: "gpt-5.5", - sessionId: "session", - sessionKey: "main", - }), - defaultModel: "gpt-5.5", - agentCfgContextTokens: 220_000, - sessionEntry, - sessionStore: { main: sessionEntry }, - sessionKey: "main", - storePath: path.join(rootDir, "sessions.json"), - isHeartbeat: false, - replyOperation: createReplyOperation(), - }); - - const compactCall = requireCompactEmbeddedPiSessionCall(); - expect(compactCall.currentTokenCount).toBe(161_077); + expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled(); }); it("uses the active run sessionFile when the session entry has no transcript path", async () => { @@ -1521,8 +1348,8 @@ describe("runMemoryFlushIfNeeded", () => { replyOperation: createReplyOperation(), }); - expect(compactEmbeddedPiSessionMock).toHaveBeenCalledTimes(1); - const compactCall = requireCompactEmbeddedPiSessionCall(); + expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledTimes(1); + const compactCall = requireCompactEmbeddedAgentSessionCall(); expect(compactCall.sessionId).toBe("session"); expect(compactCall.sessionFile).toContain("active-run-session.jsonl"); }); @@ -1580,7 +1407,7 @@ describe("runMemoryFlushIfNeeded", () => { replyOperation: createReplyOperation(), }); - const compactCall = requireCompactEmbeddedPiSessionCall(); + const compactCall = requireCompactEmbeddedAgentSessionCall(); expect(compactCall.currentTokenCount).toBeGreaterThan(100_000); }); @@ -1637,7 +1464,7 @@ describe("runMemoryFlushIfNeeded", () => { replyOperation: createReplyOperation(), }); - const compactCall = requireCompactEmbeddedPiSessionCall(); + const compactCall = requireCompactEmbeddedAgentSessionCall(); expect(compactCall.currentTokenCount).toBeGreaterThanOrEqual(96_000); }); @@ -1693,7 +1520,7 @@ describe("runMemoryFlushIfNeeded", () => { }); expect(entry).toBe(sessionEntry); - expect(compactEmbeddedPiSessionMock).not.toHaveBeenCalled(); + expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled(); }); it("does not treat raw transcript metadata bytes as token pressure", async () => { @@ -1762,7 +1589,7 @@ describe("runMemoryFlushIfNeeded", () => { }); expect(entry).toBe(sessionEntry); - expect(compactEmbeddedPiSessionMock).not.toHaveBeenCalled(); + expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled(); }); it("triggers preflight compaction when the active transcript exceeds the configured byte threshold", async () => { @@ -1811,7 +1638,7 @@ describe("runMemoryFlushIfNeeded", () => { expect(entry?.compactionCount).toBe(1); expect(replyOperation.setPhase).toHaveBeenCalledWith("preflight_compacting"); - const compactCall = requireCompactEmbeddedPiSessionCall(); + const compactCall = requireCompactEmbeddedAgentSessionCall(); expect(compactCall.sessionId).toBe("session"); expect(compactCall.trigger).toBe("budget"); expect(compactCall.currentTokenCount).toBe(10); @@ -1859,7 +1686,7 @@ describe("runMemoryFlushIfNeeded", () => { }); expect(entry).toBe(sessionEntry); - expect(compactEmbeddedPiSessionMock).not.toHaveBeenCalled(); + expect(compactEmbeddedAgentSessionMock).not.toHaveBeenCalled(); }); it("uses configured prompts and stored bootstrap warning signatures", async () => { @@ -1909,7 +1736,7 @@ describe("runMemoryFlushIfNeeded", () => { replyOperation: createReplyOperation(), }); - const flushCall = requireEmbeddedPiAgentCall(); + const flushCall = requireEmbeddedAgentCall(); expect(flushCall.prompt).toContain("Write notes."); expect(flushCall.prompt).toContain("NO_REPLY"); expect(flushCall.prompt).toContain("MEMORY.md"); diff --git a/src/auto-reply/reply/agent-runner-memory.ts b/src/auto-reply/reply/agent-runner-memory.ts index f03927e5e83..21093df5953 100644 --- a/src/auto-reply/reply/agent-runner-memory.ts +++ b/src/auto-reply/reply/agent-runner-memory.ts @@ -1,7 +1,7 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../../agents/runtime/index.js"; import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-budget.js"; import { estimateMessagesTokens } from "../../agents/compaction.js"; import { resolveAgentHarnessPolicy } from "../../agents/harness/policy.js"; @@ -13,7 +13,7 @@ import { resolveContextConfigProviderForRuntime } from "../../agents/openai-code import { classifyCompactionReason, DEFERRED_CONTEXT_ENGINE_COMPACTION_REASON, -} from "../../agents/pi-embedded-runner/compact-reasons.js"; +} from "../../agents/embedded-agent-runner/compact-reasons.js"; import { resolveSandboxConfigForAgent, resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import { derivePromptTokens, @@ -63,32 +63,32 @@ import { isRenderablePayload } from "./reply-payloads-base.js"; import type { ReplyOperation } from "./reply-run-registry.js"; import { incrementCompactionCount } from "./session-updates.js"; -type PiEmbeddedRuntime = typeof import("../../agents/pi-embedded.js"); +type EmbeddedAgentRuntime = typeof import("../../agents/embedded-agent.js"); const MAX_VISIBLE_MEMORY_FLUSH_ERROR_CHARS = 600; -const piEmbeddedRuntimeLoader = createLazyImportLoader( - () => import("../../agents/pi-embedded.js"), +const embeddedAgentRuntimeLoader = createLazyImportLoader( + () => import("../../agents/embedded-agent.js"), ); -function loadPiEmbeddedRuntime(): Promise { - return piEmbeddedRuntimeLoader.load(); +function loadEmbeddedAgentRuntime(): Promise { + return embeddedAgentRuntimeLoader.load(); } -async function compactEmbeddedPiSessionDefault( - ...args: Parameters +async function compactEmbeddedAgentSessionDefault( + ...args: Parameters ): Promise< - Awaited> + Awaited> > { - const { compactEmbeddedPiSession } = await loadPiEmbeddedRuntime(); - return await compactEmbeddedPiSession(...args); + const { compactEmbeddedAgentSession } = await loadEmbeddedAgentRuntime(); + return await compactEmbeddedAgentSession(...args); } -async function runEmbeddedPiAgentDefault( - ...args: Parameters -): Promise>> { - const { runEmbeddedPiAgent } = await loadPiEmbeddedRuntime(); - return await runEmbeddedPiAgent(...args); +async function runEmbeddedAgentDefault( + ...args: Parameters +): Promise>> { + const { runEmbeddedAgent } = await loadEmbeddedAgentRuntime(); + return await runEmbeddedAgent(...args); } async function ensureMemoryFlushTargetFile(params: { @@ -116,10 +116,10 @@ async function ensureMemoryFlushTargetFile(params: { } const memoryDeps = { - compactEmbeddedPiSession: compactEmbeddedPiSessionDefault, + compactEmbeddedAgentSession: compactEmbeddedAgentSessionDefault, runWithModelFallback, ensureSelectedAgentHarnessPlugin, - runEmbeddedPiAgent: runEmbeddedPiAgentDefault, + runEmbeddedAgent: runEmbeddedAgentDefault, ensureMemoryFlushTargetFile, registerAgentRunContext, refreshQueuedFollowupSession, @@ -141,8 +141,8 @@ export function setAgentRunnerMemoryTestDeps(overrides?: Partial { const deps = { - compactEmbeddedPiSession: memoryDeps.compactEmbeddedPiSession, + compactEmbeddedAgentSession: memoryDeps.compactEmbeddedAgentSession, incrementCompactionCount: memoryDeps.incrementCompactionCount, refreshQueuedFollowupSession: memoryDeps.refreshQueuedFollowupSession, }; @@ -813,7 +810,7 @@ export async function runPreflightCompactionIfNeeded(params: { params.sessionKey ?? params.followupRun.run.sessionKey, { storePath: params.storePath }, ); - const result = await deps.compactEmbeddedPiSession({ + const result = await deps.compactEmbeddedAgentSession({ sessionId: entry.sessionId, sessionKey: params.sessionKey, sandboxSessionKey: params.runtimePolicySessionKey, @@ -1195,7 +1192,7 @@ export async function runMemoryFlushIfNeeded(params: { runId: flushRunId, allowTransientCooldownProbe: runOptions?.allowTransientCooldownProbe, }); - const result = await memoryDeps.runEmbeddedPiAgent({ + const result = await memoryDeps.runEmbeddedAgent({ ...embeddedContext, ...senderContext, ...runBaseParams, diff --git a/src/auto-reply/reply/agent-runner-payloads.ts b/src/auto-reply/reply/agent-runner-payloads.ts index c96e7946420..583ae79e90f 100644 --- a/src/auto-reply/reply/agent-runner-payloads.ts +++ b/src/auto-reply/reply/agent-runner-payloads.ts @@ -1,6 +1,6 @@ import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload"; -import { sanitizeUserFacingText } from "../../agents/pi-embedded-helpers/sanitize-user-facing-text.js"; -import type { MessagingToolSend } from "../../agents/pi-embedded-messaging.types.js"; +import { sanitizeUserFacingText } from "../../agents/embedded-agent-helpers/sanitize-user-facing-text.js"; +import type { MessagingToolSend } from "../../agents/embedded-agent-messaging.types.js"; import type { ReplyToMode } from "../../config/types.js"; import { logVerbose } from "../../globals.js"; import { createLazyImportLoader } from "../../shared/lazy-promise.js"; diff --git a/src/auto-reply/reply/agent-runner.media-paths.test.ts b/src/auto-reply/reply/agent-runner.media-paths.test.ts index 7a2e333c4e5..d124b272d9b 100644 --- a/src/auto-reply/reply/agent-runner.media-paths.test.ts +++ b/src/auto-reply/reply/agent-runner.media-paths.test.ts @@ -2,23 +2,23 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { EmbeddedPiQueueMessageOutcome } from "../../agents/pi-embedded-runner/runs.js"; +import type { EmbeddedAgentQueueMessageOutcome } from "../../agents/embedded-agent-runner/runs.js"; import type { TemplateContext } from "../templating.js"; import type { FollowupRun, QueueSettings } from "./queue.js"; import { createMockFollowupRun, createMockTypingController } from "./test-helpers.js"; -const runEmbeddedPiAgentMock = vi.fn(); +const runEmbeddedAgentMock = vi.fn(); const runWithModelFallbackMock = vi.fn(); -const abortEmbeddedPiRunMock = vi.fn(); -const compactEmbeddedPiSessionMock = vi.fn(); -const isEmbeddedPiRunActiveMock = vi.fn(() => false); -const isEmbeddedPiRunStreamingMock = vi.fn(() => false); -const queueEmbeddedPiMessageWithOutcomeAsyncMock = vi.fn( +const abortEmbeddedAgentRunMock = vi.fn(); +const compactEmbeddedAgentSessionMock = vi.fn(); +const isEmbeddedAgentRunActiveMock = vi.fn(() => false); +const isEmbeddedAgentRunStreamingMock = vi.fn(() => false); +const queueEmbeddedAgentMessageWithOutcomeAsyncMock = vi.fn( async ( sessionId: string, _text: string, _options?: unknown, - ): Promise => ({ + ): Promise => ({ queued: false, sessionId, reason: "not_streaming", @@ -26,7 +26,7 @@ const queueEmbeddedPiMessageWithOutcomeAsyncMock = vi.fn( }), ); const resolveEmbeddedSessionLaneMock = vi.fn(); -const waitForEmbeddedPiRunEndMock = vi.fn(); +const waitForEmbeddedAgentRunEndMock = vi.fn(); const enqueueFollowupRunMock = vi.fn(); const scheduleFollowupDrainMock = vi.fn(); const refreshQueuedFollowupSessionMock = vi.fn(); @@ -45,23 +45,23 @@ vi.mock("../../agents/model-fallback.js", () => ({ Array.isArray((err as { attempts?: unknown[] }).attempts), })); -vi.mock("../../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: abortEmbeddedPiRunMock, - compactEmbeddedPiSession: compactEmbeddedPiSessionMock, - isEmbeddedPiRunActive: isEmbeddedPiRunActiveMock, - isEmbeddedPiRunStreaming: isEmbeddedPiRunStreamingMock, - queueEmbeddedPiMessageWithOutcomeAsync: queueEmbeddedPiMessageWithOutcomeAsyncMock, +vi.mock("../../agents/embedded-agent.js", () => ({ + abortEmbeddedAgentRun: abortEmbeddedAgentRunMock, + compactEmbeddedAgentSession: compactEmbeddedAgentSessionMock, + isEmbeddedAgentRunActive: isEmbeddedAgentRunActiveMock, + isEmbeddedAgentRunStreaming: isEmbeddedAgentRunStreamingMock, + queueEmbeddedAgentMessageWithOutcomeAsync: queueEmbeddedAgentMessageWithOutcomeAsyncMock, resolveEmbeddedSessionLane: resolveEmbeddedSessionLaneMock, - runEmbeddedPiAgent: runEmbeddedPiAgentMock, - waitForEmbeddedPiRunEnd: waitForEmbeddedPiRunEndMock, + runEmbeddedAgent: runEmbeddedAgentMock, + waitForEmbeddedAgentRunEnd: waitForEmbeddedAgentRunEndMock, })); -vi.mock("../../agents/pi-embedded-runner/runs.js", () => ({ - formatEmbeddedPiQueueFailureSummary: (outcome: { reason?: string; sessionId?: string }) => +vi.mock("../../agents/embedded-agent-runner/runs.js", () => ({ + formatEmbeddedAgentQueueFailureSummary: (outcome: { reason?: string; sessionId?: string }) => outcome.reason && outcome.sessionId ? `queue_message_failed reason=${outcome.reason} sessionId=${outcome.sessionId} gatewayHealth=live` : undefined, - queueEmbeddedPiMessageWithOutcomeAsync: queueEmbeddedPiMessageWithOutcomeAsyncMock, + queueEmbeddedAgentMessageWithOutcomeAsync: queueEmbeddedAgentMessageWithOutcomeAsyncMock, })); vi.mock("./queue.js", () => ({ @@ -146,23 +146,23 @@ describe("runReplyAgent media path normalization", () => { }); beforeEach(() => { - runEmbeddedPiAgentMock.mockReset(); + runEmbeddedAgentMock.mockReset(); runWithModelFallbackMock.mockReset(); - abortEmbeddedPiRunMock.mockReset(); - compactEmbeddedPiSessionMock.mockReset(); - isEmbeddedPiRunActiveMock.mockReset(); - isEmbeddedPiRunActiveMock.mockReturnValue(false); - isEmbeddedPiRunStreamingMock.mockReset(); - isEmbeddedPiRunStreamingMock.mockReturnValue(false); - queueEmbeddedPiMessageWithOutcomeAsyncMock.mockReset(); - queueEmbeddedPiMessageWithOutcomeAsyncMock.mockImplementation(async (sessionId: string) => ({ + abortEmbeddedAgentRunMock.mockReset(); + compactEmbeddedAgentSessionMock.mockReset(); + isEmbeddedAgentRunActiveMock.mockReset(); + isEmbeddedAgentRunActiveMock.mockReturnValue(false); + isEmbeddedAgentRunStreamingMock.mockReset(); + isEmbeddedAgentRunStreamingMock.mockReturnValue(false); + queueEmbeddedAgentMessageWithOutcomeAsyncMock.mockReset(); + queueEmbeddedAgentMessageWithOutcomeAsyncMock.mockImplementation(async (sessionId: string) => ({ queued: false, sessionId, reason: "not_streaming", gatewayHealth: "live", })); resolveEmbeddedSessionLaneMock.mockReset(); - waitForEmbeddedPiRunEndMock.mockReset(); + waitForEmbeddedAgentRunEndMock.mockReset(); enqueueFollowupRunMock.mockReset(); scheduleFollowupDrainMock.mockReset(); refreshQueuedFollowupSessionMock.mockReset(); @@ -196,7 +196,7 @@ describe("runReplyAgent media path normalization", () => { }); it("normalizes final MEDIA replies against the run workspace", async () => { - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "MEDIA:./out/generated.png" }], meta: { agentMeta: { @@ -230,7 +230,7 @@ describe("runReplyAgent media path normalization", () => { }); it("steers active prompts in steer queue mode", async () => { - queueEmbeddedPiMessageWithOutcomeAsyncMock.mockImplementation(async (sessionId: string) => ({ + queueEmbeddedAgentMessageWithOutcomeAsyncMock.mockImplementation(async (sessionId: string) => ({ queued: true, sessionId, target: "embedded_run", @@ -246,7 +246,7 @@ describe("runReplyAgent media path normalization", () => { }), ); - expect(queueEmbeddedPiMessageWithOutcomeAsyncMock).toHaveBeenLastCalledWith( + expect(queueEmbeddedAgentMessageWithOutcomeAsyncMock).toHaveBeenLastCalledWith( "session", "generate chart", { @@ -268,13 +268,13 @@ describe("runReplyAgent media path normalization", () => { }), ); - expect(queueEmbeddedPiMessageWithOutcomeAsyncMock).not.toHaveBeenCalled(); + expect(queueEmbeddedAgentMessageWithOutcomeAsyncMock).not.toHaveBeenCalled(); expect(enqueueFollowupRunMock).toHaveBeenCalledOnce(); expect(enqueueFollowupRunMock.mock.calls[0]?.[1].prompt).toBe("generate chart"); }); it("falls back to a queued followup when active steering is rejected", async () => { - queueEmbeddedPiMessageWithOutcomeAsyncMock.mockImplementation(async (sessionId: string) => ({ + queueEmbeddedAgentMessageWithOutcomeAsyncMock.mockImplementation(async (sessionId: string) => ({ queued: false, sessionId, reason: "runtime_rejected", @@ -306,7 +306,7 @@ describe("runReplyAgent media path normalization", () => { }; }); const onBlockReply = vi.fn(); - runEmbeddedPiAgentMock.mockImplementation( + runEmbeddedAgentMock.mockImplementation( async (params: { onBlockReply?: (payload: { text?: string; mediaUrls?: string[] }) => Promise; }) => { @@ -354,7 +354,7 @@ describe("runReplyAgent media path normalization", () => { // // After the fix, agent-runner.ts passes its media context into runAgentTurnWithFallback, so // the .runtime import path is never called from inside that function. - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [], meta: { agentMeta: { @@ -379,8 +379,8 @@ describe("runReplyAgent media path normalization", () => { expect(createReplyMediaContextRuntimeMock).not.toHaveBeenCalled(); }); - it("passes current inbound media paths as native PI images", async () => { - const tmpDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-native-pi-media-")); + it("passes current inbound media paths as native OpenClaw images", async () => { + const tmpDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-native-agent-media-")); cleanupPaths.push(tmpDir); const imagePath = path.join(tmpDir, "photo.png"); await writeFile( @@ -390,7 +390,7 @@ describe("runReplyAgent media path normalization", () => { "base64", ), ); - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { agentMeta: { @@ -419,8 +419,8 @@ describe("runReplyAgent media path normalization", () => { }), ); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); + const call = runEmbeddedAgentMock.mock.calls[0]?.[0] as | { images?: Array<{ type?: string; data?: string; mimeType?: string }>; imageOrder?: string[]; @@ -437,8 +437,8 @@ describe("runReplyAgent media path normalization", () => { expect(call?.imageOrder).toEqual(["inline"]); }); - it("does not pass recent history images as unlabeled native PI images", async () => { - const tmpDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-native-pi-history-")); + it("does not pass recent history images as unlabeled native OpenClaw images", async () => { + const tmpDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-native-agent-history-")); cleanupPaths.push(tmpDir); const imagePath = path.join(tmpDir, "recent.png"); await writeFile( @@ -448,7 +448,7 @@ describe("runReplyAgent media path normalization", () => { "base64", ), ); - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { agentMeta: { @@ -483,8 +483,8 @@ describe("runReplyAgent media path normalization", () => { }), ); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); + const call = runEmbeddedAgentMock.mock.calls[0]?.[0] as | { images?: Array<{ type?: string; data?: string; mimeType?: string }>; imageOrder?: string[]; @@ -495,7 +495,7 @@ describe("runReplyAgent media path normalization", () => { }); it("falls back to prompt refs instead of forwarding partial current media", async () => { - const tmpDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-native-pi-partial-")); + const tmpDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-native-agent-partial-")); cleanupPaths.push(tmpDir); const imagePath = path.join(tmpDir, "present.png"); await writeFile( @@ -505,7 +505,7 @@ describe("runReplyAgent media path normalization", () => { "base64", ), ); - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { agentMeta: { @@ -534,8 +534,8 @@ describe("runReplyAgent media path normalization", () => { }), ); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); + const call = runEmbeddedAgentMock.mock.calls[0]?.[0] as | { images?: Array<{ type?: string; data?: string; mimeType?: string }>; imageOrder?: string[]; diff --git a/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts b/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts index 6a8f45223c1..a1f63249887 100644 --- a/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts +++ b/src/auto-reply/reply/agent-runner.misc.runreplyagent.test.ts @@ -4,9 +4,9 @@ import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { testing as embeddedRunTesting, - abortEmbeddedPiRun, - isEmbeddedPiRunActive, -} from "../../agents/pi-embedded-runner/runs.js"; + abortEmbeddedAgentRun, + isEmbeddedAgentRunActive, +} from "../../agents/embedded-agent-runner/runs.js"; import { clearRuntimeConfigSnapshot } from "../../config/config.js"; import type { OpenClawConfig } from "../../config/config.js"; import * as sessionTypesModule from "../../config/sessions.js"; @@ -46,15 +46,15 @@ function registerMemoryFlushPlanResolverForTest(resolver: MemoryFlushPlanResolve registerMemoryCapability("memory-core", { flushPlanResolver: resolver }); } -const runEmbeddedPiAgentMock = vi.fn(); +const runEmbeddedAgentMock = vi.fn(); const runCliAgentMock = vi.fn(); const runWithModelFallbackMock = vi.fn(); const runtimeErrorMock = vi.fn(); -const abortEmbeddedPiRunMock = vi.fn(); +const abortEmbeddedAgentRunMock = vi.fn(); const clearSessionQueuesMock = vi.fn(); const refreshQueuedFollowupSessionMock = vi.fn(); const compactState = vi.hoisted(() => ({ - compactEmbeddedPiSessionMock: vi.fn(), + compactEmbeddedAgentSessionMock: vi.fn(), })); vi.mock("../../agents/model-fallback.js", () => ({ @@ -73,17 +73,17 @@ vi.mock("../../agents/model-auth.js", () => ({ resolveModelAuthMode: () => "api-key", })); -vi.mock("../../agents/pi-embedded.js", () => { +vi.mock("../../agents/embedded-agent.js", () => { return { - compactEmbeddedPiSession: (params: unknown) => - compactState.compactEmbeddedPiSessionMock(params), - queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: (params: unknown) => runEmbeddedPiAgentMock(params), - abortEmbeddedPiRun: (sessionId: string) => { - abortEmbeddedPiRunMock(sessionId); - return abortEmbeddedPiRun(sessionId); + compactEmbeddedAgentSession: (params: unknown) => + compactState.compactEmbeddedAgentSessionMock(params), + queueEmbeddedAgentMessage: vi.fn().mockReturnValue(false), + runEmbeddedAgent: (params: unknown) => runEmbeddedAgentMock(params), + abortEmbeddedAgentRun: (sessionId: string) => { + abortEmbeddedAgentRunMock(sessionId); + return abortEmbeddedAgentRun(sessionId); }, - isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActive(sessionId), + isEmbeddedAgentRunActive: (sessionId: string) => isEmbeddedAgentRunActive(sessionId), }; }); @@ -194,13 +194,13 @@ beforeEach(() => { resetSystemEventsForTest(); embeddedRunTesting.resetActiveEmbeddedRuns(); replyRunRegistryTesting.resetReplyRunRegistry(); - runEmbeddedPiAgentMock.mockClear(); + runEmbeddedAgentMock.mockClear(); runCliAgentMock.mockClear(); runWithModelFallbackMock.mockClear(); runtimeErrorMock.mockClear(); - abortEmbeddedPiRunMock.mockClear(); - compactState.compactEmbeddedPiSessionMock.mockReset(); - compactState.compactEmbeddedPiSessionMock.mockResolvedValue({ + abortEmbeddedAgentRunMock.mockClear(); + compactState.compactEmbeddedAgentSessionMock.mockReset(); + compactState.compactEmbeddedAgentSessionMock.mockResolvedValue({ compacted: false, reason: "test-preflight-disabled", }); @@ -308,7 +308,7 @@ describe("runReplyAgent auto-compaction token update", () => { await seedSessionStore({ storePath, sessionKey, entry: sessionEntry }); - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { agentMeta: params.agentMeta, @@ -383,7 +383,7 @@ describe("runReplyAgent auto-compaction token update", () => { updatedAt: Date.now(), totalTokens: 50_000, }; - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "ok" }], meta: { agentMeta: {} }, }); @@ -559,7 +559,7 @@ describe("runReplyAgent auto-compaction token update", () => { describe("runReplyAgent block streaming", () => { it("coalesces duplicate text_end block replies", async () => { const onBlockReply = vi.fn(); - runEmbeddedPiAgentMock.mockImplementationOnce(async (params) => { + runEmbeddedAgentMock.mockImplementationOnce(async (params) => { const block = params.onBlockReply as ((payload: { text?: string }) => void) | undefined; block?.({ text: "Hello" }); block?.({ text: "Hello" }); @@ -663,7 +663,7 @@ describe("runReplyAgent block streaming", () => { }); }); - runEmbeddedPiAgentMock.mockImplementationOnce(async (params) => { + runEmbeddedAgentMock.mockImplementationOnce(async (params) => { const block = params.onBlockReply as ((payload: { text?: string }) => void) | undefined; block?.({ text: "Chunk" }); return { @@ -775,7 +775,7 @@ describe("runReplyAgent Active Memory inline debug", () => { "utf-8", ); - runEmbeddedPiAgentMock.mockImplementationOnce(async () => { + runEmbeddedAgentMock.mockImplementationOnce(async () => { const latest = loadSessionStore(storePath, { skipCache: true }); latest[sessionKey] = { ...latest[sessionKey], @@ -887,7 +887,7 @@ describe("runReplyAgent Active Memory inline debug", () => { "utf-8", ); - runEmbeddedPiAgentMock.mockImplementationOnce(async () => { + runEmbeddedAgentMock.mockImplementationOnce(async () => { const latest = loadSessionStore(storePath, { skipCache: true }); latest[sessionKey] = { ...latest[sessionKey], @@ -998,7 +998,7 @@ describe("runReplyAgent Active Memory inline debug", () => { "utf-8", ); - runEmbeddedPiAgentMock.mockImplementationOnce(async () => { + runEmbeddedAgentMock.mockImplementationOnce(async () => { const latest = loadSessionStore(storePath, { skipCache: true }); latest[sessionKey] = { ...latest[sessionKey], @@ -1147,7 +1147,7 @@ describe("runReplyAgent Active Memory inline debug", () => { ], }), ); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "Visible reply" }], meta: { finalPromptText: @@ -1348,7 +1348,7 @@ describe("runReplyAgent Active Memory inline debug", () => { await fs.writeFile(storePath, JSON.stringify({ [sessionKey]: sessionEntry }, null, 2), "utf-8"); await fs.writeFile(sessionFile, "", "utf-8"); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "Visible reply" }], meta: { finalPromptText: "secret prompt context", @@ -1464,7 +1464,7 @@ describe("runReplyAgent Active Memory inline debug", () => { "utf-8", ); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "Visible reply" }], meta: { finalPromptText: "/trace raw", @@ -1564,7 +1564,7 @@ describe("runReplyAgent Active Memory inline debug", () => { await fs.writeFile(storePath, JSON.stringify({ [sessionKey]: sessionEntry }, null, 2), "utf-8"); await fs.writeFile(sessionFile, "", "utf-8"); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "Visible reply" }], meta: { finalPromptText: "show me\n~~~\nnot a fence", @@ -1669,7 +1669,7 @@ describe("runReplyAgent Active Memory inline debug", () => { ); const loadSessionStoreSpy = vi.spyOn(sessionTypesModule, "loadSessionStore"); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "Normal reply" }], meta: {}, }); @@ -1824,7 +1824,7 @@ describe("runReplyAgent claude-cli routing", () => { const result = await createRun(); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); expect(runCliAgentMock).toHaveBeenCalledTimes(1); expectReplyText(result, "ok"); }); @@ -2007,7 +2007,7 @@ describe("runReplyAgent claude-cli routing", () => { typingMode: "instant", }); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); expectRecordFields( firstMockCallArg(runCliAgentMock, "CLI run params"), { provider: "claude-cli" }, @@ -2082,7 +2082,7 @@ describe("runReplyAgent messaging tool dedupe", () => { } it("delivers distinct replies when a messaging tool sent via the same provider + target", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], messagingToolSentTexts: ["different message"], messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }], @@ -2095,7 +2095,7 @@ describe("runReplyAgent messaging tool dedupe", () => { }); it("drops duplicate replies when a messaging tool sent the same text via the same provider + target", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], messagingToolSentTexts: ["hello world!"], messagingToolSentTargets: [{ tool: "slack", provider: "slack", to: "channel:C1" }], @@ -2108,7 +2108,7 @@ describe("runReplyAgent messaging tool dedupe", () => { }); it("delivers replies when tool provider does not match", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], messagingToolSentTexts: ["different message"], messagingToolSentTargets: [{ tool: "discord", provider: "discord", to: "channel:C1" }], @@ -2121,7 +2121,7 @@ describe("runReplyAgent messaging tool dedupe", () => { }); it("keeps final reply when text matches a cross-target messaging send", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], messagingToolSentTexts: ["hello world!"], messagingToolSentTargets: [{ tool: "discord", provider: "discord", to: "channel:C1" }], @@ -2134,7 +2134,7 @@ describe("runReplyAgent messaging tool dedupe", () => { }); it("delivers replies when account ids do not match", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], messagingToolSentTexts: ["different message"], messagingToolSentTargets: [ @@ -2215,7 +2215,7 @@ describe("runReplyAgent reminder commitment guard", () => { } it("appends guard note when reminder commitment is not backed by cron.add", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "I'll remind you tomorrow morning." }], meta: {}, successfulCronAdds: 0, @@ -2229,7 +2229,7 @@ describe("runReplyAgent reminder commitment guard", () => { }); it("keeps reminder commitment unchanged when cron.add succeeded", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "I'll remind you tomorrow morning." }], meta: {}, successfulCronAdds: 1, @@ -2254,7 +2254,7 @@ describe("runReplyAgent reminder commitment guard", () => { ], }); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "I'll ping you when it's done." }], meta: {}, successfulCronAdds: 0, @@ -2279,7 +2279,7 @@ describe("runReplyAgent reminder commitment guard", () => { ], }); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "I'll remind you tomorrow morning." }], meta: {}, successfulCronAdds: 0, @@ -2307,7 +2307,7 @@ describe("runReplyAgent reminder commitment guard", () => { ], }); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "I'll check back in an hour." }], meta: {}, successfulCronAdds: 0, @@ -2335,7 +2335,7 @@ describe("runReplyAgent reminder commitment guard", () => { ], }); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "I'll ping you later." }], meta: {}, successfulCronAdds: 0, @@ -2351,7 +2351,7 @@ describe("runReplyAgent reminder commitment guard", () => { it("still appends guard note when cron store read fails", async () => { loadCronStoreMock.mockRejectedValueOnce(new Error("store read failed")); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "I'll remind you after lunch." }], meta: {}, successfulCronAdds: 0, @@ -2366,7 +2366,7 @@ describe("runReplyAgent reminder commitment guard", () => { }); describe("runReplyAgent fallback reasoning tags", () => { - type EmbeddedPiAgentParams = { + type EmbeddedAgentParams = { enforceFinalTag?: boolean; prompt?: string; }; @@ -2439,7 +2439,7 @@ describe("runReplyAgent fallback reasoning tags", () => { } it("enforces when the fallback provider requires reasoning tags", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: {}, }); @@ -2453,7 +2453,10 @@ describe("runReplyAgent fallback reasoning tags", () => { await createRun(); - const call = firstMockCallArg(runEmbeddedPiAgentMock, "PI run params") as EmbeddedPiAgentParams; + const call = firstMockCallArg( + runEmbeddedAgentMock, + "embedded run params", + ) as EmbeddedAgentParams; expect(call.enforceFinalTag).toBe(true); }); @@ -2466,7 +2469,7 @@ describe("runReplyAgent fallback reasoning tags", () => { systemPrompt: "Flush memory into the configured memory file.", relativePath: "memory/active.md", })); - runEmbeddedPiAgentMock.mockImplementation(async (params: EmbeddedPiAgentParams) => { + runEmbeddedAgentMock.mockImplementation(async (params: EmbeddedAgentParams) => { if (params.prompt?.includes("Pre-compaction memory flush.")) { return { payloads: [], meta: {} }; } @@ -2477,7 +2480,7 @@ describe("runReplyAgent fallback reasoning tags", () => { provider: "google-gemini-cli", model: "gemini-3", })); - compactState.compactEmbeddedPiSessionMock.mockResolvedValueOnce({ + compactState.compactEmbeddedAgentSessionMock.mockResolvedValueOnce({ ok: true, compacted: true, result: { tokensAfter: 1_000_000 }, @@ -2492,11 +2495,9 @@ describe("runReplyAgent fallback reasoning tags", () => { }, }); - const flushCall = runEmbeddedPiAgentMock.mock.calls.find(([params]) => - (params as EmbeddedPiAgentParams | undefined)?.prompt?.includes( - "Pre-compaction memory flush.", - ), - )?.[0] as EmbeddedPiAgentParams | undefined; + const flushCall = runEmbeddedAgentMock.mock.calls.find(([params]) => + (params as EmbeddedAgentParams | undefined)?.prompt?.includes("Pre-compaction memory flush."), + )?.[0] as EmbeddedAgentParams | undefined; expect(flushCall?.enforceFinalTag).toBe(true); }); @@ -2578,7 +2579,7 @@ describe("runReplyAgent response usage footer", () => { } it("appends session key when responseUsage=full", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: { agentMeta: { @@ -2598,8 +2599,8 @@ describe("runReplyAgent response usage footer", () => { expect(text).toContain(`· session \`${sessionKey}\``); }); - it("does not append session key or cost when responseUsage=tokens", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + it("does not append session key when responseUsage=tokens", async () => { + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: { agentMeta: { @@ -2641,7 +2642,7 @@ describe("runReplyAgent response usage footer", () => { }); it("shows configured costs for aws-sdk providers when responseUsage=full", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: { agentMeta: { @@ -2692,7 +2693,7 @@ describe("runReplyAgent response usage footer", () => { describe("runReplyAgent transient HTTP retry", () => { it("retries once after transient 521 HTML failure and then succeeds", async () => { vi.useFakeTimers(); - runEmbeddedPiAgentMock + runEmbeddedAgentMock .mockRejectedValueOnce( new Error( `521 Web server is downCloudflare`, @@ -2759,7 +2760,7 @@ describe("runReplyAgent transient HTTP retry", () => { await vi.advanceTimersByTimeAsync(2_500); const result = await runPromise; - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(2); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(2); expect(runtimeErrorMock).toHaveBeenCalledWith( 'Transient HTTP provider error before reply (521 Web server is downCloudflare). Retrying once in 2500ms.', ); @@ -2775,7 +2776,7 @@ describe("runReplyAgent billing error classification", () => { // matches context overflow heuristics. This test verifies the final user-visible // message is the billing-specific one, not the "Context overflow" fallback. it("returns billing message for mixed-signal error (billing text + overflow patterns)", async () => { - runEmbeddedPiAgentMock.mockRejectedValueOnce( + runEmbeddedAgentMock.mockRejectedValueOnce( new Error("402 Payment Required: request token limit exceeded for this billing plan"), ); @@ -2895,7 +2896,7 @@ describe("runReplyAgent mid-turn rate-limit fallback", () => { } it("surfaces a final error when only reasoning preceded a mid-turn rate limit", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "reasoning", isReasoning: true }], meta: { error: { @@ -2912,7 +2913,7 @@ describe("runReplyAgent mid-turn rate-limit fallback", () => { }); it("preserves successful media-only replies that use legacy mediaUrl", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ mediaUrl: "https://example.test/image.png" }], meta: { error: { diff --git a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts index 5c257303c1a..85c60549be8 100644 --- a/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts +++ b/src/auto-reply/reply/agent-runner.runreplyagent.e2e.test.ts @@ -29,9 +29,9 @@ type AgentRunParams = { }; const state = vi.hoisted(() => ({ - compactEmbeddedPiSessionMock: vi.fn(), - queueEmbeddedPiMessageMock: vi.fn(), - runEmbeddedPiAgentMock: vi.fn(), + compactEmbeddedAgentSessionMock: vi.fn(), + queueEmbeddedAgentMessageMock: vi.fn(), + runEmbeddedAgentMock: vi.fn(), })); function countMatching(items: readonly T[], predicate: (item: T) => boolean): number { @@ -94,15 +94,15 @@ vi.mock("../../agents/model-fallback.js", () => ({ Array.isArray((err as { attempts?: unknown[] }).attempts), })); -vi.mock("../../agents/pi-embedded.js", () => ({ - compactEmbeddedPiSession: (params: unknown) => state.compactEmbeddedPiSessionMock(params), - queueEmbeddedPiMessage: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: (params: unknown) => state.runEmbeddedPiAgentMock(params), +vi.mock("../../agents/embedded-agent.js", () => ({ + compactEmbeddedAgentSession: (params: unknown) => state.compactEmbeddedAgentSessionMock(params), + queueEmbeddedAgentMessage: vi.fn().mockReturnValue(false), + runEmbeddedAgent: (params: unknown) => state.runEmbeddedAgentMock(params), })); -vi.mock("../../agents/pi-embedded-runner/runs.js", () => ({ - queueEmbeddedPiMessage: (sessionId: string, prompt: string, options: unknown) => - state.queueEmbeddedPiMessageMock(sessionId, prompt, options), +vi.mock("../../agents/embedded-agent-runner/runs.js", () => ({ + queueEmbeddedAgentMessage: (sessionId: string, prompt: string, options: unknown) => + state.queueEmbeddedAgentMessageMock(sessionId, prompt, options), })); vi.mock("./queue.js", () => ({ @@ -120,19 +120,19 @@ beforeAll(async () => { beforeEach(() => { replyRunTesting.resetReplyRunRegistry(); - state.compactEmbeddedPiSessionMock.mockReset(); - state.compactEmbeddedPiSessionMock.mockResolvedValue({ + state.compactEmbeddedAgentSessionMock.mockReset(); + state.compactEmbeddedAgentSessionMock.mockResolvedValue({ ok: true, compacted: false, reason: "test-default", }); - state.runEmbeddedPiAgentMock.mockReset(); - state.runEmbeddedPiAgentMock.mockResolvedValue({ + state.runEmbeddedAgentMock.mockReset(); + state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "final" }], meta: { agentMeta: { usage: { input: 1, output: 1 } } }, }); - state.queueEmbeddedPiMessageMock.mockReset(); - state.queueEmbeddedPiMessageMock.mockReturnValue(false); + state.queueEmbeddedAgentMessageMock.mockReset(); + state.queueEmbeddedAgentMessageMock.mockReturnValue(false); vi.mocked(enqueueFollowupRun).mockClear(); vi.mocked(refreshQueuedFollowupSession).mockClear(); vi.mocked(scheduleFollowupDrain).mockClear(); @@ -247,7 +247,7 @@ describe("runReplyAgent heartbeat followup guard", () => { const result = await run(); expect(result).toBeUndefined(); - expect(state.runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(state.runEmbeddedAgentMock).not.toHaveBeenCalled(); expect(typing.cleanup).toHaveBeenCalledTimes(1); active.complete(); }); @@ -282,8 +282,8 @@ describe("runReplyAgent heartbeat followup guard", () => { active.complete(); await pending; - expect(state.runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); - const [call] = mockCallArgs(state.runEmbeddedPiAgentMock, "run embedded pi agent"); + expect(state.runEmbeddedAgentMock).toHaveBeenCalledTimes(1); + const [call] = mockCallArgs(state.runEmbeddedAgentMock, "run embedded agent"); expect((call as AgentRunParams).sessionId).toBe("post-compact-session"); expect((call as AgentRunParams).sessionFile).toBe("/tmp/post-compact.jsonl"); }); @@ -300,7 +300,7 @@ describe("runReplyAgent heartbeat followup guard", () => { const result = await run(); expect(result).toBeUndefined(); - expect(state.runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(state.runEmbeddedAgentMock).not.toHaveBeenCalled(); expect(typing.cleanup).toHaveBeenCalledTimes(1); }); @@ -316,12 +316,12 @@ describe("runReplyAgent heartbeat followup guard", () => { expect(result).toBeUndefined(); expect(vi.mocked(enqueueFollowupRun)).not.toHaveBeenCalled(); - expect(state.runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(state.runEmbeddedAgentMock).not.toHaveBeenCalled(); expect(typing.cleanup).toHaveBeenCalledTimes(1); }); it("drops heartbeat runs before steering active streams", async () => { - state.queueEmbeddedPiMessageMock.mockReturnValueOnce(true); + state.queueEmbeddedAgentMessageMock.mockReturnValueOnce(true); const { run, typing } = createMinimalRun({ opts: { isHeartbeat: true }, isActive: true, @@ -334,9 +334,9 @@ describe("runReplyAgent heartbeat followup guard", () => { const result = await run(); expect(result).toBeUndefined(); - expect(state.queueEmbeddedPiMessageMock).not.toHaveBeenCalled(); + expect(state.queueEmbeddedAgentMessageMock).not.toHaveBeenCalled(); expect(vi.mocked(enqueueFollowupRun)).not.toHaveBeenCalled(); - expect(state.runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(state.runEmbeddedAgentMock).not.toHaveBeenCalled(); expect(typing.cleanup).toHaveBeenCalledTimes(1); }); @@ -352,7 +352,7 @@ describe("runReplyAgent heartbeat followup guard", () => { expect(result).toBeUndefined(); expect(vi.mocked(enqueueFollowupRun)).toHaveBeenCalledTimes(1); - expect(state.runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(state.runEmbeddedAgentMock).not.toHaveBeenCalled(); }); it("keeps typing alive when a followup is queued behind a live active run", async () => { @@ -369,7 +369,7 @@ describe("runReplyAgent heartbeat followup guard", () => { expect(result).toBeUndefined(); expect(vi.mocked(enqueueFollowupRun)).toHaveBeenCalledTimes(1); expect(vi.mocked(scheduleFollowupDrain)).not.toHaveBeenCalled(); - expect(state.runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(state.runEmbeddedAgentMock).not.toHaveBeenCalled(); expect(typing.startTypingLoop).toHaveBeenCalledTimes(1); expect(typing.refreshTypingTtl).toHaveBeenCalledTimes(1); expect(typing.cleanup).not.toHaveBeenCalled(); @@ -389,7 +389,7 @@ describe("runReplyAgent heartbeat followup guard", () => { expect(result).toBeUndefined(); expect(vi.mocked(enqueueFollowupRun)).toHaveBeenCalledTimes(1); expect(vi.mocked(scheduleFollowupDrain)).toHaveBeenCalledTimes(1); - expect(state.runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(state.runEmbeddedAgentMock).not.toHaveBeenCalled(); expect(typing.cleanup).toHaveBeenCalledTimes(1); }); @@ -398,7 +398,7 @@ describe("runReplyAgent heartbeat followup guard", () => { const persistSpy = vi .spyOn(accounting, "persistRunSessionUsage") .mockRejectedValueOnce(new Error("persist exploded")); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: { agentMeta: { usage: { input: 1, output: 1 } } }, }); @@ -433,7 +433,7 @@ describe("runReplyAgent pending final delivery capture", () => { }; const sessionStore = { main: sessionEntry }; const storePath = await createSessionStoreFile(sessionEntry); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "private final" }], meta: {}, }); @@ -461,7 +461,7 @@ describe("runReplyAgent pending final delivery capture", () => { }; const sessionStore = { main: sessionEntry }; const storePath = await createSessionStoreFile(sessionEntry); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "denied final" }], meta: {}, }); @@ -487,7 +487,7 @@ describe("runReplyAgent pending final delivery capture", () => { }; const sessionStore = { main: sessionEntry }; const storePath = await createSessionStoreFile(sessionEntry); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hidden reasoning", isReasoning: true }, { text: "visible final" }], meta: {}, }); @@ -513,7 +513,7 @@ describe("runReplyAgent pending final delivery capture", () => { }; const sessionStore = { main: sessionEntry }; const storePath = await createSessionStoreFile(sessionEntry); - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "Sent daily summary to channel." }], meta: {}, }); @@ -544,7 +544,7 @@ describe("runReplyAgent pending final delivery capture", () => { const sessionStore = { main: sessionEntry }; const storePath = await createSessionStoreFile(sessionEntry); const longRemainder = "Sent daily digest to channel. ".repeat(12).trimEnd(); // ~360 chars, > 300 - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: `HEARTBEAT_OK ${longRemainder}` }], meta: {}, }); @@ -568,7 +568,7 @@ describe("runReplyAgent pending final delivery capture", () => { describe("runReplyAgent typing (heartbeat)", () => { it("signals typing for normal runs", async () => { const onPartialReply = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onPartialReply?.({ text: "hi" }); return { payloads: [{ text: "final" }], meta: {} }; }); @@ -585,7 +585,7 @@ describe("runReplyAgent typing (heartbeat)", () => { it("never signals typing for heartbeat runs", async () => { const onPartialReply = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onPartialReply?.({ text: "hi" }); return { payloads: [{ text: "final" }], meta: {} }; }); @@ -611,7 +611,7 @@ describe("runReplyAgent typing (heartbeat)", () => { "utf-8", ); try { - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "HEARTBEAT_OK" }], meta: {}, }); @@ -664,7 +664,7 @@ describe("runReplyAgent typing (heartbeat)", () => { for (const testCase of cases) { const onPartialReply = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { for (const text of testCase.partials) { await params.onPartialReply?.({ text }); } @@ -700,7 +700,7 @@ describe("runReplyAgent typing (heartbeat)", () => { it("keeps final text blocks after partial preview streaming", async () => { const onPartialReply = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onPartialReply?.({ text: "First block\n\nSecond block" }); return { payloads: [{ text: "First block" }, { text: "Second block" }], @@ -726,7 +726,7 @@ describe("runReplyAgent typing (heartbeat)", () => { const onPartialReply = vi.fn(); const onBlockReply = vi.fn(); const onReasoningStream = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { expect(params.silentExpected).toBe(true); await params.onReasoningStream?.({ text: "Reasoning:\nI am trying to send NO_REPLY now." }); await params.onPartialReply?.({ text: "I am trying to send NO_REPLY now." }); @@ -751,7 +751,7 @@ describe("runReplyAgent typing (heartbeat)", () => { const onPartialReply = vi.fn(); const onBlockReply = vi.fn(); const onReasoningStream = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { expect(params.silentExpected).toBe(true); await params.onReasoningStream?.({ text: "Reasoning:\nNO_REPLY" }); await params.onPartialReply?.({ text: "NO_REPLY" }); @@ -773,7 +773,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("does not start typing on assistant message start without prior text in message mode", async () => { - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onAssistantMessageStart?.(); return { payloads: [{ text: "final" }], meta: {} }; }); @@ -788,7 +788,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("starts typing from reasoning stream in thinking mode", async () => { - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onReasoningStream?.({ text: "Reasoning:\n_step_" }); await params.onPartialReply?.({ text: "hi" }); return { payloads: [{ text: "final" }], meta: {} }; @@ -806,7 +806,7 @@ describe("runReplyAgent typing (heartbeat)", () => { it("keeps assistant partial streaming enabled when reasoning mode is stream", async () => { const onPartialReply = vi.fn(); const onReasoningStream = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onReasoningStream?.({ text: "Reasoning:\n_step_" }); await params.onPartialReply?.({ text: "answer chunk" }); return { payloads: [{ text: "final" }], meta: {} }; @@ -823,7 +823,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("suppresses typing in never mode", async () => { - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onPartialReply?.({ text: "hi" }); return { payloads: [{ text: "final" }], meta: {} }; }); @@ -839,7 +839,7 @@ describe("runReplyAgent typing (heartbeat)", () => { it("signals typing on normalized block replies", async () => { const onBlockReply = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onBlockReply?.({ text: "\n\nchunk", mediaUrls: [] }); return { payloads: [{ text: "final" }], meta: {} }; }); @@ -863,7 +863,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("strips workflow function response scaffolding from final delivery", async () => { - state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({ + state.runEmbeddedAgentMock.mockImplementationOnce(async () => ({ payloads: [ { text: [ @@ -906,7 +906,7 @@ describe("runReplyAgent typing (heartbeat)", () => { for (const testCase of cases) { const onToolResult = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onToolResult?.({ text: testCase.toolText, mediaUrls: [] }); return { payloads: [{ text: "final" }], meta: {} }; }); @@ -936,7 +936,7 @@ describe("runReplyAgent typing (heartbeat)", () => { it("preserves channelData on forwarded tool results", async () => { const onToolResult = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onToolResult?.({ text: "Approval required.\n\n```txt\n/approve 117ba06d allow-once\n```", channelData: { @@ -970,7 +970,7 @@ describe("runReplyAgent typing (heartbeat)", () => { it("forwards media-only tool results without typing text", async () => { const onToolResult = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onToolResult?.({ mediaUrls: ["/tmp/generated.png"], }); @@ -996,7 +996,7 @@ describe("runReplyAgent typing (heartbeat)", () => { it("retries transient HTTP failures once with timer-driven backoff", async () => { vi.useFakeTimers(); let calls = 0; - state.runEmbeddedPiAgentMock.mockImplementation(async () => { + state.runEmbeddedAgentMock.mockImplementation(async () => { calls += 1; if (calls === 1) { throw new Error("502 Bad Gateway"); @@ -1028,7 +1028,7 @@ describe("runReplyAgent typing (heartbeat)", () => { updatedAt: Date.now(), }; const sessionStore = { main: sessionEntry }; - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "final" }], meta: {}, }); @@ -1098,7 +1098,7 @@ describe("runReplyAgent typing (heartbeat)", () => { const storePath = join(storeRoot, "sessions.json"); await writeFile(storePath, JSON.stringify(sessionStore), "utf-8"); try { - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "subagent timed out" }], meta: { agentMeta: { @@ -1181,7 +1181,7 @@ describe("runReplyAgent typing (heartbeat)", () => { model: "gpt-5.5", }; const sessionStore = { main: sessionEntry }; - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {}, }); @@ -1245,7 +1245,7 @@ describe("runReplyAgent typing (heartbeat)", () => { const sessionStore = { main: sessionEntry }; const onBlockReply = vi.fn(); - state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { + state.runEmbeddedAgentMock.mockImplementationOnce(async (params: AgentRunParams) => { await params.onBlockReply?.({ text: "streamed answer" }); return { payloads: [], meta: {} }; }); @@ -1293,7 +1293,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }; const sessionStore = { main: sessionEntry }; - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "final" }], meta: {}, }); @@ -1343,7 +1343,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("surfaces a configured backend failure when fallback produces no visible reply", async () => { - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "NO_REPLY" }], meta: {}, }); @@ -1390,7 +1390,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("surfaces a configured backend failure when fallback returns no payloads", async () => { - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {}, }); @@ -1447,7 +1447,7 @@ describe("runReplyAgent typing (heartbeat)", () => { modelOverrideFallbackOriginModel: "gemma-4-e4b-it", }; const sessionStore = { main: sessionEntry }; - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "NO_REPLY" }], meta: {}, }); @@ -1475,7 +1475,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("announces fallback without silence failure when fallback already replied through a messaging tool", async () => { - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "already sent" }], messagingToolSentTexts: ["already sent"], messagingToolSentTargets: [{ tool: "message", provider: "discord", to: "channel:C1" }], @@ -1527,7 +1527,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("does not treat whitespace-only messaging evidence as fallback delivery", async () => { - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "NO_REPLY" }], messagingToolSentTexts: [" "], messagingToolSentMediaUrls: ["\t"], @@ -1581,7 +1581,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("announces fallback without silence failure when fallback already completed a cron side effect", async () => { - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "NO_REPLY" }], successfulCronAdds: 1, meta: {}, @@ -1632,7 +1632,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("announces fallback without silence failure when fallback committed target-only messaging delivery", async () => { - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "NO_REPLY" }], messagingToolSentTargets: [{ tool: "message", provider: "discord", to: "channel:C1" }], meta: {}, @@ -1683,7 +1683,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("announces fallback without silence failure when fallback already delivered an approval prompt", async () => { - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], didSendDeterministicApprovalPrompt: true, meta: {}, @@ -1731,7 +1731,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("preserves intentional fallback silence when the turn permits silent replies", async () => { - state.runEmbeddedPiAgentMock.mockResolvedValueOnce({ + state.runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "NO_REPLY" }], meta: {}, }); @@ -1783,7 +1783,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }; const sessionStore = { main: sessionEntry }; - state.runEmbeddedPiAgentMock.mockResolvedValue({ + state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "final" }], meta: {}, }); @@ -1839,7 +1839,7 @@ describe("runReplyAgent typing (heartbeat)", () => { const sessionStore = { main: sessionEntry }; let callCount = 0; - state.runEmbeddedPiAgentMock.mockResolvedValue({ + state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "final" }], meta: {}, }); @@ -1909,7 +1909,7 @@ describe("runReplyAgent typing (heartbeat)", () => { const sessionStore = { main: sessionEntry }; let callCount = 0; - state.runEmbeddedPiAgentMock.mockResolvedValue({ + state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "final" }], meta: {}, }); @@ -1989,7 +1989,7 @@ describe("runReplyAgent typing (heartbeat)", () => { const sessionStore = { main: sessionEntry }; let callCount = 0; - state.runEmbeddedPiAgentMock.mockResolvedValue({ + state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "final" }], meta: {}, }); @@ -2089,7 +2089,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }; const sessionStore = { main: sessionEntry }; - state.runEmbeddedPiAgentMock.mockResolvedValue({ + state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "final" }], meta: {}, }); @@ -2140,7 +2140,7 @@ describe("runReplyAgent typing (heartbeat)", () => { const storePath = join(dir, "sessions.json"); await writeFile(storePath, JSON.stringify({ main: sessionEntry }), "utf8"); - state.runEmbeddedPiAgentMock.mockResolvedValue({ + state.runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "final" }], meta: { agentMeta: { @@ -2184,7 +2184,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("surfaces overflow fallback when embedded run returns empty payloads", async () => { - state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({ + state.runEmbeddedAgentMock.mockImplementationOnce(async () => ({ payloads: [], meta: { durationMs: 1, @@ -2207,7 +2207,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("surfaces overflow fallback when embedded payload text is whitespace-only", async () => { - state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({ + state.runEmbeddedAgentMock.mockImplementationOnce(async () => ({ payloads: [{ text: " \n\t ", isError: true }], meta: { durationMs: 1, @@ -2230,7 +2230,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("returns friendly message for role ordering errors thrown as exceptions", async () => { - state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => { + state.runEmbeddedAgentMock.mockImplementationOnce(async () => { throw new Error("400 Incorrect role information"); }); @@ -2243,7 +2243,7 @@ describe("runReplyAgent typing (heartbeat)", () => { }); it("rewrites Bun socket errors into friendly text", async () => { - state.runEmbeddedPiAgentMock.mockImplementationOnce(async () => ({ + state.runEmbeddedAgentMock.mockImplementationOnce(async () => ({ payloads: [ { text: "TypeError: The socket connection was closed unexpectedly. For more information, pass `verbose: true` in the second argument to fetch()", diff --git a/src/auto-reply/reply/agent-runner.ts b/src/auto-reply/reply/agent-runner.ts index 4e65f1351df..059d888a201 100644 --- a/src/auto-reply/reply/agent-runner.ts +++ b/src/auto-reply/reply/agent-runner.ts @@ -7,12 +7,12 @@ import { } from "../../agents/agent-scope.js"; import { resolveContextTokensForModel } from "../../agents/context.js"; import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js"; +import { + formatEmbeddedAgentQueueFailureSummary, + queueEmbeddedAgentMessageWithOutcomeAsync, +} from "../../agents/embedded-agent-runner/runs.js"; import { resolveModelAuthMode } from "../../agents/model-auth.js"; import { isCliProvider } from "../../agents/model-selection.js"; -import { - formatEmbeddedPiQueueFailureSummary, - queueEmbeddedPiMessageWithOutcomeAsync, -} from "../../agents/pi-embedded-runner/runs.js"; import { deriveContextPromptTokens, hasNonzeroUsage, normalizeUsage } from "../../agents/usage.js"; import { enqueueCommitmentExtraction } from "../../commitments/runtime.js"; import type { OpenClawConfig } from "../../config/config.js"; @@ -1152,7 +1152,7 @@ export async function runReplyAgent(params: { const steerSessionId = (sessionKey ? replyRunRegistry.resolveSessionId(sessionKey) : undefined) ?? followupRun.run.sessionId; - const steerOutcome = await queueEmbeddedPiMessageWithOutcomeAsync( + const steerOutcome = await queueEmbeddedAgentMessageWithOutcomeAsync( steerSessionId, followupRun.prompt, { @@ -1165,7 +1165,7 @@ export async function runReplyAgent(params: { typing.cleanup(); return undefined; } - const summary = formatEmbeddedPiQueueFailureSummary(steerOutcome); + const summary = formatEmbeddedAgentQueueFailureSummary(steerOutcome); logVerbose(`queue: active session ${steerSessionId} rejected steering injection: ${summary}`); } diff --git a/src/auto-reply/reply/commands-abort-trigger.test.ts b/src/auto-reply/reply/commands-abort-trigger.test.ts index aab7cb87fc2..03bb39d989c 100644 --- a/src/auto-reply/reply/commands-abort-trigger.test.ts +++ b/src/auto-reply/reply/commands-abort-trigger.test.ts @@ -4,13 +4,13 @@ import { handleAbortTrigger } from "./commands-session-abort.js"; import "./commands-session-abort.test-support.js"; import type { HandleCommandsParams } from "./commands-types.js"; -const abortEmbeddedPiRunMock = vi.hoisted(() => vi.fn()); +const abortEmbeddedAgentRunMock = vi.hoisted(() => vi.fn()); const persistAbortTargetEntryMock = vi.hoisted(() => vi.fn()); const setAbortMemoryMock = vi.hoisted(() => vi.fn()); const abortSessionRunTargetMock = vi.hoisted(() => vi.fn()); -vi.mock("../../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: abortEmbeddedPiRunMock, +vi.mock("../../agents/embedded-agent.js", () => ({ + abortEmbeddedAgentRun: abortEmbeddedAgentRunMock, })); vi.mock("../../globals.js", () => ({ @@ -96,7 +96,7 @@ describe("handleAbortTrigger", () => { const result = await handleAbortTrigger(buildAbortParams(), true); expect(result).toEqual({ shouldContinue: false }); expect(abortSessionRunTargetMock).not.toHaveBeenCalled(); - expect(abortEmbeddedPiRunMock).not.toHaveBeenCalled(); + expect(abortEmbeddedAgentRunMock).not.toHaveBeenCalled(); expect(persistAbortTargetEntryMock).not.toHaveBeenCalled(); expect(setAbortMemoryMock).not.toHaveBeenCalled(); }); diff --git a/src/auto-reply/reply/commands-compact.runtime.ts b/src/auto-reply/reply/commands-compact.runtime.ts index a29f30caa1d..df585368d0c 100644 --- a/src/auto-reply/reply/commands-compact.runtime.ts +++ b/src/auto-reply/reply/commands-compact.runtime.ts @@ -1,9 +1,9 @@ export { - abortEmbeddedPiRun, - compactEmbeddedPiSession, - isEmbeddedPiRunActive, - waitForEmbeddedPiRunEnd, -} from "../../agents/pi-embedded.js"; + abortEmbeddedAgentRun, + compactEmbeddedAgentSession, + isEmbeddedAgentRunActive, + waitForEmbeddedAgentRunEnd, +} from "../../agents/embedded-agent.js"; export { resolveFreshSessionTotalTokens, resolveSessionFilePath, diff --git a/src/auto-reply/reply/commands-compact.test.ts b/src/auto-reply/reply/commands-compact.test.ts index faaa00ff1be..3b0e176dd33 100644 --- a/src/auto-reply/reply/commands-compact.test.ts +++ b/src/auto-reply/reply/commands-compact.test.ts @@ -7,21 +7,21 @@ import { import type { HandleCommandsParams } from "./commands-types.js"; vi.mock("./commands-compact.runtime.js", () => ({ - abortEmbeddedPiRun: vi.fn(), - compactEmbeddedPiSession: vi.fn(), + abortEmbeddedAgentRun: vi.fn(), + compactEmbeddedAgentSession: vi.fn(), enqueueSystemEvent: vi.fn(), formatContextUsageShort: vi.fn(() => "Context 12.1k"), formatTokenCount: vi.fn((value: number) => `${value}`), incrementCompactionCount: vi.fn(), - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunActive: vi.fn().mockReturnValue(false), resolveFreshSessionTotalTokens: vi.fn(() => 12_345), resolveSessionFilePath: vi.fn(() => "/tmp/session.json"), resolveSessionFilePathOptions: vi.fn(() => ({})), - waitForEmbeddedPiRunEnd: vi.fn().mockResolvedValue(undefined), + waitForEmbeddedAgentRunEnd: vi.fn().mockResolvedValue(undefined), })); const { - compactEmbeddedPiSession, + compactEmbeddedAgentSession, formatContextUsageShort, incrementCompactionCount, resolveSessionFilePathOptions, @@ -54,10 +54,10 @@ function buildCompactParams( } as unknown as HandleCommandsParams; } -function requireCompactEmbeddedPiSessionCall(index = 0) { - const call = vi.mocked(compactEmbeddedPiSession).mock.calls[index]?.[0]; +function requireCompactEmbeddedAgentSessionCall(index = 0) { + const call = vi.mocked(compactEmbeddedAgentSession).mock.calls[index]?.[0]; if (!call) { - throw new Error(`compactEmbeddedPiSession call ${index} missing`); + throw new Error(`compactEmbeddedAgentSession call ${index} missing`); } return call; } @@ -107,7 +107,7 @@ describe("handleCompactCommand", () => { ); expect(result).toBeNull(); - expect(vi.mocked(compactEmbeddedPiSession)).not.toHaveBeenCalled(); + expect(vi.mocked(compactEmbeddedAgentSession)).not.toHaveBeenCalled(); }); it("rejects unauthorized /compact commands", async () => { @@ -129,11 +129,11 @@ describe("handleCompactCommand", () => { ); expect(result).toEqual({ shouldContinue: false }); - expect(vi.mocked(compactEmbeddedPiSession)).not.toHaveBeenCalled(); + expect(vi.mocked(compactEmbeddedAgentSession)).not.toHaveBeenCalled(); }); it("routes manual compaction with explicit trigger and context metadata", async () => { - vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({ + vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({ ok: true, compacted: false, }); @@ -171,8 +171,8 @@ describe("handleCompactCommand", () => { ); expect(result?.shouldContinue).toBe(false); - expect(vi.mocked(compactEmbeddedPiSession)).toHaveBeenCalledOnce(); - const call = requireCompactEmbeddedPiSessionCall(); + expect(vi.mocked(compactEmbeddedAgentSession)).toHaveBeenCalledOnce(); + const call = requireCompactEmbeddedAgentSessionCall(); expect(call.sessionId).toBe("session-1"); expect(call.sessionKey).toBe("agent:main:main"); expect(call.allowGatewaySubagentBinding).toBe(true); @@ -218,7 +218,7 @@ describe("handleCompactCommand", () => { }); it("uses the canonical session agent when resolving the compaction session file", async () => { - vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({ + vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({ ok: true, compacted: false, }); @@ -253,7 +253,7 @@ describe("handleCompactCommand", () => { }); it("uses the canonical session agent directory for compaction runtime inputs", async () => { - vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({ + vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({ ok: true, compacted: false, }); @@ -278,7 +278,7 @@ describe("handleCompactCommand", () => { true, ); - expect(requireCompactEmbeddedPiSessionCall().agentDir).toBe("/tmp/target-agent"); + expect(requireCompactEmbeddedAgentSessionCall().agentDir).toBe("/tmp/target-agent"); expect(resolveAgentDirMock).toHaveBeenCalledOnce(); const [configArg, agentIdArg] = requireResolveAgentDirCall(); expect(configArg).toBe(cfg); @@ -286,7 +286,7 @@ describe("handleCompactCommand", () => { }); it("prefers the target session entry for compaction runtime metadata", async () => { - vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({ + vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({ ok: true, compacted: false, }); @@ -324,7 +324,7 @@ describe("handleCompactCommand", () => { true, ); - const call = requireCompactEmbeddedPiSessionCall(); + const call = requireCompactEmbeddedAgentSessionCall(); expect(call.sessionId).toBe("target-session"); expect(call.groupId).toBe("target-group"); expect(call.groupChannel).toBe("#target"); @@ -334,7 +334,7 @@ describe("handleCompactCommand", () => { }); it("prefers the target session entry when incrementing compaction count", async () => { - vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({ + vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({ ok: true, compacted: true, result: { @@ -410,7 +410,7 @@ describe("handleCompactCommand", () => { }); it("resolves /compact context budget from the active Codex runtime config instead of stale session metadata", async () => { - vi.mocked(compactEmbeddedPiSession).mockResolvedValueOnce({ + vi.mocked(compactEmbeddedAgentSession).mockResolvedValueOnce({ ok: true, compacted: true, result: { @@ -455,7 +455,7 @@ describe("handleCompactCommand", () => { true, ); - expect(requireCompactEmbeddedPiSessionCall().contextTokenBudget).toBe(258_000); + expect(requireCompactEmbeddedAgentSessionCall().contextTokenBudget).toBe(258_000); expect(vi.mocked(formatContextUsageShort)).toHaveBeenLastCalledWith(56_000, 258_000); }); }); diff --git a/src/auto-reply/reply/commands-compact.ts b/src/auto-reply/reply/commands-compact.ts index b26a913ea01..c98cab3c12b 100644 --- a/src/auto-reply/reply/commands-compact.ts +++ b/src/auto-reply/reply/commands-compact.ts @@ -217,9 +217,9 @@ export const handleCompactCommand: CommandHandler = async (params) => { } const runtime = await loadCompactRuntime(); const sessionId = targetSessionEntry.sessionId; - if (runtime.isEmbeddedPiRunActive(sessionId)) { - runtime.abortEmbeddedPiRun(sessionId); - await runtime.waitForEmbeddedPiRunEnd(sessionId, 15_000); + if (runtime.isEmbeddedAgentRunActive(sessionId)) { + runtime.abortEmbeddedAgentRun(sessionId); + await runtime.waitForEmbeddedAgentRunEnd(sessionId, 15_000); } const sessionAgentId = params.sessionKey ? resolveSessionAgentId({ sessionKey: params.sessionKey, config: params.cfg }) @@ -245,7 +245,7 @@ export const handleCompactCommand: CommandHandler = async (params) => { liveContextTokens: params.contextTokens, persistedContextTokens: targetSessionEntry.contextTokens, }); - const result = await runtime.compactEmbeddedPiSession({ + const result = await runtime.compactEmbeddedAgentSession({ sessionId, sessionKey: params.sessionKey, allowGatewaySubagentBinding: true, diff --git a/src/auto-reply/reply/commands-context-report.ts b/src/auto-reply/reply/commands-context-report.ts index c48c0786c5f..41db4fe980c 100644 --- a/src/auto-reply/reply/commands-context-report.ts +++ b/src/auto-reply/reply/commands-context-report.ts @@ -3,7 +3,7 @@ import { analyzeBootstrapBudget } from "../../agents/bootstrap-budget.js"; import { resolveBootstrapMaxChars, resolveBootstrapTotalMaxChars, -} from "../../agents/pi-embedded-helpers/bootstrap.js"; +} from "../../agents/embedded-agent-helpers/bootstrap.js"; import { buildSystemPromptReport } from "../../agents/system-prompt-report.js"; import { resolveFreshSessionTotalTokens, diff --git a/src/auto-reply/reply/commands-export-session.test.ts b/src/auto-reply/reply/commands-export-session.test.ts index e59f10e38e3..86ab441867a 100644 --- a/src/auto-reply/reply/commands-export-session.test.ts +++ b/src/auto-reply/reply/commands-export-session.test.ts @@ -61,7 +61,7 @@ vi.mock("node:fs", async () => { if (filePath.includes("/export-html/")) { return actual.readFileSync(filePath, "utf8"); } - return ""; + return actual.readFileSync(filePath, "utf8"); }), }; return { diff --git a/src/auto-reply/reply/commands-export-session.ts b/src/auto-reply/reply/commands-export-session.ts index dae49f3b295..9ca8f31ff37 100644 --- a/src/auto-reply/reply/commands-export-session.ts +++ b/src/auto-reply/reply/commands-export-session.ts @@ -1,11 +1,12 @@ import fsp from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import type { - FileEntry as PiSessionFileEntry, - SessionEntry as PiSessionEntry, - SessionHeader, -} from "@earendil-works/pi-coding-agent"; +import { + migrateSessionEntries, + type FileEntry as SessionFileEntry, + type SessionEntry as AgentSessionEntry, + type SessionHeader, +} from "../../agents/sessions/session-manager.js"; import { pathExists } from "../../infra/fs-safe.js"; import { isRecord } from "../../shared/record-coerce.js"; import type { ReplyPayload } from "../types.js"; @@ -22,7 +23,7 @@ const EXPORT_HTML_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), interface SessionData { header: SessionHeader | null; - entries: PiSessionEntry[]; + entries: AgentSessionEntry[]; leafId: string | null; systemPrompt?: string; tools?: Array<{ name: string; description?: string; parameters?: unknown }>; @@ -43,11 +44,6 @@ async function loadTemplate(fileName: string): Promise { return await fsp.readFile(path.join(EXPORT_HTML_DIR, fileName), "utf-8"); } -async function migratePiSessionEntries(fileEntries: PiSessionFileEntry[]): Promise { - const { migrateSessionEntries } = await import("@earendil-works/pi-coding-agent"); - migrateSessionEntries(fileEntries); -} - function replaceHtmlPlaceholder(template: string, name: string, value: string): string { let replaced = false; const placeholder = new RegExp( @@ -76,7 +72,7 @@ async function generateHtml(sessionData: SessionData): Promise { loadTemplate(path.join("vendor", "highlight.min.js")), ]); - // Use pi-mono dark theme colors (matching their theme/dark.json) + // Use the bundled dark session-export palette const themeVars = ` --cyan: #00d7ff; --blue: #5f87ff; @@ -161,7 +157,7 @@ async function writeNewDefaultExportFile(filePath: string, html: string): Promis throw new Error(`Could not find an unused export filename near ${filePath}`); } -function isSessionFileEntry(value: unknown): value is PiSessionFileEntry { +function isSessionFileEntry(value: unknown): value is SessionFileEntry { if (!isRecord(value) || typeof value.type !== "string") { return false; } @@ -173,10 +169,10 @@ function isSessionFileEntry(value: unknown): value is PiSessionFileEntry { } function parseSessionEntriesWithWarnings(content: string): { - entries: PiSessionFileEntry[]; + entries: SessionFileEntry[]; warnings: SessionExportJsonlWarning[]; } { - const entries: PiSessionFileEntry[] = []; + const entries: SessionFileEntry[] = []; const warnings: SessionExportJsonlWarning[] = []; const rows = content.split(/\r?\n/u); for (const [index, rawLine] of rows.entries()) { @@ -241,16 +237,16 @@ function formatSessionExportWarning(summary: SessionExportWarningSummary): strin async function readSessionDataFromTranscript(sessionFile: string): Promise<{ header: SessionHeader | null; - entries: PiSessionEntry[]; + entries: AgentSessionEntry[]; leafId: string | null; warnings: SessionExportWarningSummary[]; }> { const raw = await fsp.readFile(sessionFile, "utf-8"); const { entries: fileEntries, warnings } = parseSessionEntriesWithWarnings(raw); - await migratePiSessionEntries(fileEntries); + migrateSessionEntries(fileEntries); const header = fileEntries.find((entry): entry is SessionHeader => entry.type === "session") ?? null; - const entries = fileEntries.filter((entry): entry is PiSessionEntry => entry.type !== "session"); + const entries = fileEntries.filter((entry): entry is AgentSessionEntry => entry.type !== "session"); const lastEntry = entries.at(-1); const leafId = typeof lastEntry?.id === "string" ? lastEntry.id : null; return { header, entries, leafId, warnings: summarizeSessionExportWarnings(warnings) }; diff --git a/src/auto-reply/reply/commands-models.test.ts b/src/auto-reply/reply/commands-models.test.ts index 77906281426..1c63d9603c8 100644 --- a/src/auto-reply/reply/commands-models.test.ts +++ b/src/auto-reply/reply/commands-models.test.ts @@ -480,13 +480,13 @@ describe("handleModelsCommand", () => { description: "Use the OpenAI Codex runtime selected by the effective harness policy.", }); expect(data.runtimeChoicesByProvider?.get("openai")?.[1]).toEqual({ - id: "pi", - label: "OpenClaw Pi Default", - description: "Use the built-in OpenClaw Pi runtime.", + id: "openclaw", + label: "OpenClaw Default", + description: "Use the built-in OpenClaw runtime.", }); }); - it("keeps custom OpenAI-compatible providers on the Pi default runtime choice", async () => { + it("keeps custom OpenAI-compatible providers on the OpenClaw default runtime choice", async () => { const data = await buildModelsProviderData({ models: { providers: { @@ -504,9 +504,9 @@ describe("handleModelsCommand", () => { } as OpenClawConfig); expect(data.runtimeChoicesByProvider?.get("openai")?.[0]).toEqual({ - id: "pi", - label: "OpenClaw Pi Default", - description: "Use the built-in OpenClaw Pi runtime.", + id: "openclaw", + label: "OpenClaw Default", + description: "Use the built-in OpenClaw runtime.", }); }); @@ -516,7 +516,7 @@ describe("handleModelsCommand", () => { providers: { openai: { baseUrl: "https://api.openai.com/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, @@ -537,9 +537,9 @@ describe("handleModelsCommand", () => { description: "Use the OpenAI Codex runtime selected by the effective harness policy.", }); expect(data.runtimeChoicesByProvider?.get("openai")?.[1]).toEqual({ - id: "pi", - label: "OpenClaw Pi Default", - description: "Use the built-in OpenClaw Pi runtime.", + id: "openclaw", + label: "OpenClaw Default", + description: "Use the built-in OpenClaw runtime.", }); }); @@ -562,9 +562,9 @@ describe("handleModelsCommand", () => { } as OpenClawConfig); expect(data.runtimeChoicesByProvider?.get("anthropic")?.[0]).toEqual({ - id: "pi", - label: "OpenClaw Pi Default", - description: "Use the built-in OpenClaw Pi runtime.", + id: "openclaw", + label: "OpenClaw Default", + description: "Use the built-in OpenClaw runtime.", }); }); @@ -592,9 +592,9 @@ describe("handleModelsCommand", () => { description: "Use the Claude CLI runtime selected by the effective harness policy.", }); expect(data.runtimeChoicesByProvider?.get("anthropic")?.[1]).toEqual({ - id: "pi", - label: "OpenClaw Pi Default", - description: "Use the built-in OpenClaw Pi runtime.", + id: "openclaw", + label: "OpenClaw Default", + description: "Use the built-in OpenClaw runtime.", }); }); diff --git a/src/auto-reply/reply/commands-models.ts b/src/auto-reply/reply/commands-models.ts index 571b307a466..727b9d25534 100644 --- a/src/auto-reply/reply/commands-models.ts +++ b/src/auto-reply/reply/commands-models.ts @@ -89,7 +89,7 @@ function usesUnfilteredCatalogModels(provider: string): boolean { function normalizeRuntimeChoiceId(runtime: string | undefined): string { const normalized = normalizeLowercaseStringOrEmpty(runtime); if (!normalized || normalized === "auto" || normalized === "default") { - return "pi"; + return "openclaw"; } return normalized; } @@ -106,8 +106,8 @@ function buildRuntimeChoice(params: { id, label, description: - id === "pi" - ? "Use the built-in OpenClaw Pi runtime." + id === "openclaw" + ? "Use the built-in OpenClaw runtime." : params.cli ? `Run ${params.provider} models through ${label}.` : `Use the ${label} runtime selected by the effective harness policy.`, @@ -304,7 +304,7 @@ export async function buildModelsProviderData( modelId: defaultModelId, }), ]; - addRuntimeChoice(choices, buildRuntimeChoice({ cfg, provider, runtime: "pi" })); + addRuntimeChoice(choices, buildRuntimeChoice({ cfg, provider, runtime: "openclaw" })); addRuntimeChoice( choices, buildRuntimeChoice({ diff --git a/src/auto-reply/reply/commands-status.test.ts b/src/auto-reply/reply/commands-status.test.ts index 47f7059cf49..59d9bad800d 100644 --- a/src/auto-reply/reply/commands-status.test.ts +++ b/src/auto-reply/reply/commands-status.test.ts @@ -44,10 +44,10 @@ vi.mock("../../infra/provider-usage.js", async (importOriginal) => { }; }); -vi.mock("../../agents/harness/builtin-pi.js", () => ({ - createPiAgentHarness: () => ({ - id: "pi", - label: "OpenClaw Pi", +vi.mock("../../agents/harness/builtin-openclaw.js", () => ({ + createOpenClawAgentHarness: () => ({ + id: "openclaw", + label: "OpenClaw Default", supports: () => ({ supported: true, priority: 0 }), runAttempt: async () => { throw new Error("not used in status tests"); @@ -562,7 +562,7 @@ describe("buildStatusReply subagent summary", () => { expect(normalizeTestText(text)).toContain("Uptime: gateway 2h 5m · system 4d 3h"); }); - it("shows the effective non-PI embedded harness in /status", async () => { + it("shows the effective non-OpenClaw embedded harness in /status", async () => { registerStatusCodexHarness(); const text = await buildStatusText({ @@ -719,7 +719,7 @@ describe("buildStatusReply subagent summary", () => { ); }); - it("uses Codex OAuth auth labels for explicit OpenAI PI auth order", async () => { + it("uses Codex OAuth auth labels for explicit OpenAI OpenClaw auth order", async () => { await withTempHome( async (dir) => { const authPath = path.join( @@ -760,7 +760,7 @@ describe("buildStatusReply subagent summary", () => { defaults: { models: { "openai/gpt-5.5": { - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, }, }, }, @@ -772,7 +772,7 @@ describe("buildStatusReply subagent summary", () => { }, }, sessionEntry: { - sessionId: "sess-status-openai-pi-codex-oauth", + sessionId: "sess-status-openai-agent-codex-oauth", updatedAt: 0, }, sessionKey: "agent:main:main", @@ -782,7 +782,7 @@ describe("buildStatusReply subagent summary", () => { provider: "openai", model: "gpt-5.5", contextTokens: 32_000, - resolvedHarness: "pi", + resolvedHarness: "openclaw", resolvedFastMode: false, resolvedVerboseLevel: "off", resolvedReasoningLevel: "off", @@ -1079,7 +1079,7 @@ describe("buildStatusReply subagent summary", () => { } }); - it("keeps /status on a session-pinned PI harness after config changes", async () => { + it("keeps /status on a session-pinned OpenClaw harness after config changes", async () => { registerStatusCodexHarness(); const text = await buildStatusText({ @@ -1092,10 +1092,10 @@ describe("buildStatusReply subagent summary", () => { }, }, sessionEntry: { - sessionId: "sess-status-pinned-pi", + sessionId: "sess-status-pinned-agent", updatedAt: 0, fastMode: true, - agentHarnessId: "pi", + agentHarnessId: "openclaw", }, sessionKey: "agent:main:main", parentSessionKey: "agent:main:main", diff --git a/src/auto-reply/reply/commands-steer.runtime.ts b/src/auto-reply/reply/commands-steer.runtime.ts index 24a9013ffa3..9682f90191f 100644 --- a/src/auto-reply/reply/commands-steer.runtime.ts +++ b/src/auto-reply/reply/commands-steer.runtime.ts @@ -1,7 +1,7 @@ export { - formatEmbeddedPiQueueFailureSummary, - isEmbeddedPiRunActive, - queueEmbeddedPiMessage, - queueEmbeddedPiMessageWithOutcomeAsync, + formatEmbeddedAgentQueueFailureSummary, + isEmbeddedAgentRunActive, + queueEmbeddedAgentMessage, + queueEmbeddedAgentMessageWithOutcomeAsync, resolveActiveEmbeddedRunSessionId, -} from "../../agents/pi-embedded-runner/runs.js"; +} from "../../agents/embedded-agent-runner/runs.js"; diff --git a/src/auto-reply/reply/commands-steer.test.ts b/src/auto-reply/reply/commands-steer.test.ts index 06f01b727aa..761c34ca1e0 100644 --- a/src/auto-reply/reply/commands-steer.test.ts +++ b/src/auto-reply/reply/commands-steer.test.ts @@ -3,9 +3,9 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { buildCommandTestParams } from "./commands.test-harness.js"; const steerRuntimeMocks = vi.hoisted(() => ({ - formatEmbeddedPiQueueFailureSummary: vi.fn(), - isEmbeddedPiRunActive: vi.fn(), - queueEmbeddedPiMessageWithOutcomeAsync: vi.fn(), + formatEmbeddedAgentQueueFailureSummary: vi.fn(), + isEmbeddedAgentRunActive: vi.fn(), + queueEmbeddedAgentMessageWithOutcomeAsync: vi.fn(), resolveActiveEmbeddedRunSessionId: vi.fn(), })); @@ -24,13 +24,13 @@ function buildParams(commandBody: string) { describe("handleSteerCommand", () => { beforeEach(() => { - steerRuntimeMocks.formatEmbeddedPiQueueFailureSummary + steerRuntimeMocks.formatEmbeddedAgentQueueFailureSummary .mockReset() .mockReturnValue( "queue_message_failed reason=not_streaming sessionId=session-active gatewayHealth=live", ); - steerRuntimeMocks.isEmbeddedPiRunActive.mockReset().mockReturnValue(false); - steerRuntimeMocks.queueEmbeddedPiMessageWithOutcomeAsync.mockReset().mockResolvedValue({ + steerRuntimeMocks.isEmbeddedAgentRunActive.mockReset().mockReturnValue(false); + steerRuntimeMocks.queueEmbeddedAgentMessageWithOutcomeAsync.mockReset().mockResolvedValue({ queued: true, sessionId: "session-active", target: "embedded_run", @@ -51,7 +51,7 @@ describe("handleSteerCommand", () => { expect(steerRuntimeMocks.resolveActiveEmbeddedRunSessionId).toHaveBeenCalledWith( "agent:main:main", ); - expect(steerRuntimeMocks.queueEmbeddedPiMessageWithOutcomeAsync).toHaveBeenCalledWith( + expect(steerRuntimeMocks.queueEmbeddedAgentMessageWithOutcomeAsync).toHaveBeenCalledWith( "session-active", "keep going", { @@ -74,7 +74,7 @@ describe("handleSteerCommand", () => { expect(steerRuntimeMocks.resolveActiveEmbeddedRunSessionId).toHaveBeenCalledWith( "agent:main:discord:direct:target", ); - expect(steerRuntimeMocks.queueEmbeddedPiMessageWithOutcomeAsync).toHaveBeenCalledWith( + expect(steerRuntimeMocks.queueEmbeddedAgentMessageWithOutcomeAsync).toHaveBeenCalledWith( "session-target", "check the target", { @@ -85,7 +85,7 @@ describe("handleSteerCommand", () => { }); it("falls back to the stored session id when it is still active", async () => { - steerRuntimeMocks.isEmbeddedPiRunActive.mockReturnValue(true); + steerRuntimeMocks.isEmbeddedAgentRunActive.mockReturnValue(true); const params = buildParams("/tell continue from state"); params.sessionEntry = { sessionId: "stored-session-id", updatedAt: Date.now() }; @@ -95,8 +95,8 @@ describe("handleSteerCommand", () => { expect(steerRuntimeMocks.resolveActiveEmbeddedRunSessionId).toHaveBeenCalledWith( "agent:main:main", ); - expect(steerRuntimeMocks.isEmbeddedPiRunActive).toHaveBeenCalledWith("stored-session-id"); - expect(steerRuntimeMocks.queueEmbeddedPiMessageWithOutcomeAsync).toHaveBeenCalledWith( + expect(steerRuntimeMocks.isEmbeddedAgentRunActive).toHaveBeenCalledWith("stored-session-id"); + expect(steerRuntimeMocks.queueEmbeddedAgentMessageWithOutcomeAsync).toHaveBeenCalledWith( "stored-session-id", "continue from state", { @@ -113,7 +113,7 @@ describe("handleSteerCommand", () => { shouldContinue: false, reply: { text: "Usage: /steer " }, }); - expect(steerRuntimeMocks.queueEmbeddedPiMessageWithOutcomeAsync).not.toHaveBeenCalled(); + expect(steerRuntimeMocks.queueEmbeddedAgentMessageWithOutcomeAsync).not.toHaveBeenCalled(); }); it("continues as a normal prompt when no current session run is active", async () => { @@ -127,12 +127,12 @@ describe("handleSteerCommand", () => { expect(params.ctx.BodyForAgent).toBe("keep going"); expect((params.ctx as Record).BodyStripped).toBe("keep going"); expect(params.command.commandBodyNormalized).toBe("keep going"); - expect(steerRuntimeMocks.queueEmbeddedPiMessageWithOutcomeAsync).not.toHaveBeenCalled(); + expect(steerRuntimeMocks.queueEmbeddedAgentMessageWithOutcomeAsync).not.toHaveBeenCalled(); }); it("continues as a normal prompt when the active run rejects steering injection", async () => { steerRuntimeMocks.resolveActiveEmbeddedRunSessionId.mockReturnValue("session-active"); - steerRuntimeMocks.queueEmbeddedPiMessageWithOutcomeAsync.mockResolvedValue({ + steerRuntimeMocks.queueEmbeddedAgentMessageWithOutcomeAsync.mockResolvedValue({ queued: false, sessionId: "session-active", reason: "not_streaming", @@ -147,7 +147,7 @@ describe("handleSteerCommand", () => { }); expect(params.ctx.BodyForAgent).toBe("keep going"); expect(params.command.commandBodyNormalized).toBe("keep going"); - expect(steerRuntimeMocks.formatEmbeddedPiQueueFailureSummary).toHaveBeenCalledWith({ + expect(steerRuntimeMocks.formatEmbeddedAgentQueueFailureSummary).toHaveBeenCalledWith({ queued: false, sessionId: "session-active", reason: "not_streaming", @@ -157,7 +157,7 @@ describe("handleSteerCommand", () => { it("continues as a normal prompt when steering throws", async () => { steerRuntimeMocks.resolveActiveEmbeddedRunSessionId.mockReturnValue("session-active"); - steerRuntimeMocks.queueEmbeddedPiMessageWithOutcomeAsync.mockRejectedValue( + steerRuntimeMocks.queueEmbeddedAgentMessageWithOutcomeAsync.mockRejectedValue( new Error("socket closed"), ); @@ -173,7 +173,7 @@ describe("handleSteerCommand", () => { it("continues as a normal prompt when the active run is compacting", async () => { steerRuntimeMocks.resolveActiveEmbeddedRunSessionId.mockReturnValue("session-active"); - steerRuntimeMocks.queueEmbeddedPiMessageWithOutcomeAsync.mockResolvedValue({ + steerRuntimeMocks.queueEmbeddedAgentMessageWithOutcomeAsync.mockResolvedValue({ queued: false, sessionId: "session-active", reason: "compacting", diff --git a/src/auto-reply/reply/commands-steer.ts b/src/auto-reply/reply/commands-steer.ts index d97b36743a8..2de05b42048 100644 --- a/src/auto-reply/reply/commands-steer.ts +++ b/src/auto-reply/reply/commands-steer.ts @@ -8,9 +8,9 @@ import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { isNativeCommandTurn, resolveCommandTurnContext } from "../command-turn-context.js"; import { rejectUnauthorizedCommand } from "./command-gates.js"; import { - formatEmbeddedPiQueueFailureSummary, - isEmbeddedPiRunActive, - queueEmbeddedPiMessageWithOutcomeAsync, + formatEmbeddedAgentQueueFailureSummary, + isEmbeddedAgentRunActive, + queueEmbeddedAgentMessageWithOutcomeAsync, resolveActiveEmbeddedRunSessionId, } from "./commands-steer.runtime.js"; import type { @@ -67,7 +67,7 @@ function resolveSteerSessionId(params: { const entry = resolveStoredSessionEntry(params.commandParams, params.targetSessionKey); const sessionId = normalizeOptionalString(entry?.sessionId); - if (!sessionId || !isEmbeddedPiRunActive(sessionId)) { + if (!sessionId || !isEmbeddedAgentRunActive(sessionId)) { return undefined; } return sessionId; @@ -139,7 +139,7 @@ export const handleSteerCommand: CommandHandler = async (params, allowTextComman ); } - const queueOutcome = await queueEmbeddedPiMessageWithOutcomeAsync(sessionId, message, { + const queueOutcome = await queueEmbeddedAgentMessageWithOutcomeAsync(sessionId, message, { steeringMode: "all", debounceMs: 0, }).catch((err: unknown): CommandHandlerResult => { @@ -153,7 +153,7 @@ export const handleSteerCommand: CommandHandler = async (params, allowTextComman return queueOutcome; } if (!queueOutcome.queued) { - const summary = formatEmbeddedPiQueueFailureSummary(queueOutcome); + const summary = formatEmbeddedAgentQueueFailureSummary(queueOutcome); return continueWithSteerFallback( params, message, diff --git a/src/auto-reply/reply/commands-stop-target.test.ts b/src/auto-reply/reply/commands-stop-target.test.ts index dfc9f8d3ddd..5530ec87c32 100644 --- a/src/auto-reply/reply/commands-stop-target.test.ts +++ b/src/auto-reply/reply/commands-stop-target.test.ts @@ -12,15 +12,15 @@ import { handleStopCommand } from "./commands-session-abort.js"; import "./commands-session-abort.test-support.js"; import type { HandleCommandsParams } from "./commands-types.js"; -const abortEmbeddedPiRunMock = vi.hoisted(() => vi.fn()); +const abortEmbeddedAgentRunMock = vi.hoisted(() => vi.fn()); const createInternalHookEventMock = vi.hoisted(() => vi.fn(() => ({}))); const persistAbortTargetEntryMock = vi.hoisted(() => vi.fn(async () => true)); const resolveSessionIdMock = vi.hoisted(() => vi.fn(() => undefined)); const stopSubagentsForRequesterMock = vi.hoisted(() => vi.fn(() => ({ stopped: 0 }))); const abortSessionRunTargetMock = vi.hoisted(() => vi.fn()); -vi.mock("../../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: abortEmbeddedPiRunMock, +vi.mock("../../agents/embedded-agent.js", () => ({ + abortEmbeddedAgentRun: abortEmbeddedAgentRunMock, })); vi.mock("../../globals.js", () => ({ @@ -156,7 +156,7 @@ describe("handleStopCommand target fallback", () => { key: "agent:target:telegram:direct:123", sessionId: undefined, }); - expect(abortEmbeddedPiRunMock).not.toHaveBeenCalledWith("wrapper-session-id"); + expect(abortEmbeddedAgentRunMock).not.toHaveBeenCalledWith("wrapper-session-id"); const [[persistAbortTargetParams]] = persistAbortTargetEntryMock.mock.calls as unknown as Array< [ { diff --git a/src/auto-reply/reply/commands-system-prompt.test.ts b/src/auto-reply/reply/commands-system-prompt.test.ts index 922dd599a9d..081038961b7 100644 --- a/src/auto-reply/reply/commands-system-prompt.test.ts +++ b/src/auto-reply/reply/commands-system-prompt.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { resolveSessionAgentIds } from "../../agents/agent-scope.js"; +import { createOpenClawCodingTools } from "../../agents/agent-tools.js"; import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js"; -import { createOpenClawCodingTools } from "../../agents/pi-tools.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import { buildAgentSystemPrompt } from "../../agents/system-prompt.js"; import { resolveCommandsSystemPromptBundle } from "./commands-system-prompt.js"; @@ -52,7 +52,7 @@ vi.mock("../../agents/system-prompt.js", () => ({ buildAgentSystemPrompt: vi.fn(() => "system prompt"), })); -vi.mock("../../agents/pi-tools.js", () => ({ +vi.mock("../../agents/agent-tools.js", () => ({ createOpenClawCodingTools: createOpenClawCodingToolsMock, })); diff --git a/src/auto-reply/reply/commands-system-prompt.ts b/src/auto-reply/reply/commands-system-prompt.ts index 895446f2bce..cd1ff12ac1e 100644 --- a/src/auto-reply/reply/commands-system-prompt.ts +++ b/src/auto-reply/reply/commands-system-prompt.ts @@ -1,13 +1,13 @@ -import type { AgentTool } from "@earendil-works/pi-agent-core"; import { isAcpRuntimeSpawnAvailable } from "../../acp/runtime/availability.js"; import { resolveSessionAgentIds } from "../../agents/agent-scope.js"; +import { createOpenClawCodingTools } from "../../agents/agent-tools.js"; import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js"; +import type { EmbeddedContextFile } from "../../agents/embedded-agent-helpers.js"; +import { resolveEmbeddedFullAccessState } from "../../agents/embedded-agent-runner/sandbox-info.js"; import { canExecRequestNode } from "../../agents/exec-defaults.js"; import { resolveDefaultModelForAgent } from "../../agents/model-selection.js"; -import type { EmbeddedContextFile } from "../../agents/pi-embedded-helpers.js"; -import { resolveEmbeddedFullAccessState } from "../../agents/pi-embedded-runner/sandbox-info.js"; -import { createOpenClawCodingTools } from "../../agents/pi-tools.js"; import { resolveAgentPromptSurfaceForSessionKey } from "../../agents/prompt-surface.js"; +import type { AgentTool } from "../../agents/runtime/index.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox.js"; import { buildWorkspaceSkillSnapshot } from "../../agents/skills.js"; import { getSkillsSnapshotVersion } from "../../agents/skills/refresh-state.js"; diff --git a/src/auto-reply/reply/commands-types.ts b/src/auto-reply/reply/commands-types.ts index c22be8a57e6..662dfa38827 100644 --- a/src/auto-reply/reply/commands-types.ts +++ b/src/auto-reply/reply/commands-types.ts @@ -1,4 +1,4 @@ -import type { BlockReplyChunking } from "../../agents/pi-embedded-block-chunker.js"; +import type { BlockReplyChunking } from "../../agents/embedded-agent-block-chunker.js"; import type { SkillCommandSpec } from "../../agents/skills.js"; import type { ChannelId } from "../../channels/plugins/types.public.js"; import type { SessionEntry, SessionScope } from "../../config/sessions.js"; diff --git a/src/auto-reply/reply/conversation-label-generator.test.ts b/src/auto-reply/reply/conversation-label-generator.test.ts index a0f4127c1eb..a63f594ca4e 100644 --- a/src/auto-reply/reply/conversation-label-generator.test.ts +++ b/src/auto-reply/reply/conversation-label-generator.test.ts @@ -8,9 +8,9 @@ const resolveDefaultModelForAgent = vi.hoisted(() => vi.fn()); const resolveModelAsync = vi.hoisted(() => vi.fn()); const prepareModelForSimpleCompletion = vi.hoisted(() => vi.fn()); -vi.mock("@earendil-works/pi-ai", async () => { +vi.mock("openclaw/plugin-sdk/llm", async () => { const original = - await vi.importActual("@earendil-works/pi-ai"); + await vi.importActual("openclaw/plugin-sdk/llm"); return { ...original, completeSimple, @@ -25,7 +25,7 @@ vi.mock("../../agents/model-selection.js", () => ({ resolveDefaultModelForAgent, })); -vi.mock("../../agents/pi-embedded-runner/model.js", () => ({ +vi.mock("../../agents/embedded-agent-runner/model.js", () => ({ resolveModelAsync, })); diff --git a/src/auto-reply/reply/conversation-label-generator.ts b/src/auto-reply/reply/conversation-label-generator.ts index 9b97a4bf72e..25f9938145f 100644 --- a/src/auto-reply/reply/conversation-label-generator.ts +++ b/src/auto-reply/reply/conversation-label-generator.ts @@ -1,7 +1,7 @@ -import { completeSimple, type TextContent } from "@earendil-works/pi-ai"; +import { completeSimple, type TextContent } from "openclaw/plugin-sdk/llm"; +import { resolveModelAsync } from "../../agents/embedded-agent-runner/model.js"; import { requireApiKey } from "../../agents/model-auth.js"; import { resolveDefaultModelForAgent } from "../../agents/model-selection.js"; -import { resolveModelAsync } from "../../agents/pi-embedded-runner/model.js"; import { prepareModelForSimpleCompletion } from "../../agents/simple-completion-transport.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { logVerbose } from "../../globals.js"; diff --git a/src/auto-reply/reply/current-turn-images.ts b/src/auto-reply/reply/current-turn-images.ts index fbb551010cb..ffbdfc40286 100644 --- a/src/auto-reply/reply/current-turn-images.ts +++ b/src/auto-reply/reply/current-turn-images.ts @@ -1,4 +1,4 @@ -import type { ImageContent } from "@earendil-works/pi-ai"; +import type { ImageContent } from "openclaw/plugin-sdk/llm"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { logVerbose } from "../../globals.js"; import { formatErrorMessage } from "../../infra/errors.js"; @@ -126,7 +126,7 @@ export async function resolveCurrentTurnImages(params: { ); if (images.length < undescribedImageAttachments.length) { logVerbose( - `agent-runner: native PI media resolution produced ${images.length}/${undescribedImageAttachments.length} current image attachment(s); falling back to prompt image refs`, + `agent-runner: native OpenClaw media resolution produced ${images.length}/${undescribedImageAttachments.length} current image attachment(s); falling back to prompt image refs`, ); return { images: params.images, imageOrder: params.imageOrder }; } diff --git a/src/auto-reply/reply/directive-handling.model.test.ts b/src/auto-reply/reply/directive-handling.model.test.ts index 9db8bbf9837..0052a58077b 100644 --- a/src/auto-reply/reply/directive-handling.model.test.ts +++ b/src/auto-reply/reply/directive-handling.model.test.ts @@ -701,7 +701,7 @@ describe("/model chat UX", () => { expect(reply?.text).not.toContain("via codex runtime"); }); - it("does not borrow Codex auth when OpenAI model policy pins PI runtime", async () => { + it("does not borrow Codex auth when OpenAI model policy pins OpenClaw runtime", async () => { setAuthProfiles({ "openai-codex:patrick@example.test": { type: "oauth", @@ -725,7 +725,7 @@ describe("/model chat UX", () => { model: { primary: "openai/gpt-5.5" }, models: { "openai/gpt-5.5": { - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, }, }, }, @@ -1086,7 +1086,7 @@ describe("/model chat UX", () => { const { sessionEntry } = await persistModelDirectiveForTest({ command: "/model openai/gpt-4o --runtime claude-cli hello", allowedModelKeys: ["openai/gpt-4o"], - sessionEntry: createSessionEntry({ agentRuntimeOverride: "pi" }), + sessionEntry: createSessionEntry({ agentRuntimeOverride: "openclaw" }), provider: "openai", model: "gpt-4o", initialModelLabel: "openai/gpt-4o", diff --git a/src/auto-reply/reply/directive-handling.persist.ts b/src/auto-reply/reply/directive-handling.persist.ts index 6e34c83ab56..4c63fe7deb1 100644 --- a/src/auto-reply/reply/directive-handling.persist.ts +++ b/src/auto-reply/reply/directive-handling.persist.ts @@ -52,8 +52,8 @@ function resolveModelRuntimeOverride(params: { if (MODEL_RUNTIME_CLEAR_VALUES.has(runtime)) { return { kind: "clear" }; } - if (runtime === "pi") { - return { kind: "set", runtime: "pi" }; + if (runtime === "openclaw") { + return { kind: "set", runtime: "openclaw" }; } const provider = normalizeProviderId(params.provider); diff --git a/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts b/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts index 59213c78253..57fc5c524c8 100644 --- a/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts +++ b/src/auto-reply/reply/dispatch-from-config.shared.test-harness.ts @@ -253,7 +253,7 @@ vi.mock("../../infra/agent-events.js", () => ({ emitAgentEvent: (params: unknown) => agentEventMocks.emitAgentEvent(params), onAgentEvent: (listener: unknown) => agentEventMocks.onAgentEvent(listener), })); -vi.mock("./runtime-plugins.runtime.js", () => ({ +vi.mock("../../plugins/runtime-plugins.runtime.js", () => ({ ensureRuntimePluginsLoaded: runtimePluginMocks.ensureRuntimePluginsLoaded, })); vi.mock("./conversation-binding-input.js", () => { diff --git a/src/auto-reply/reply/dispatch-from-config.test.ts b/src/auto-reply/reply/dispatch-from-config.test.ts index bf50b3a3cb7..37df79a317a 100644 --- a/src/auto-reply/reply/dispatch-from-config.test.ts +++ b/src/auto-reply/reply/dispatch-from-config.test.ts @@ -499,7 +499,7 @@ vi.mock("./reply-media-paths.runtime.js", () => ({ createReplyMediaPathNormalizer: (params: unknown) => replyMediaPathMocks.createReplyMediaPathNormalizer(params), })); -vi.mock("./runtime-plugins.runtime.js", () => ({ +vi.mock("../../plugins/runtime-plugins.runtime.js", () => ({ ensureRuntimePluginsLoaded: runtimePluginMocks.ensureRuntimePluginsLoaded, })); vi.mock("./conversation-binding-input.js", () => ({ diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index 1bf154a3688..3d19902e7f1 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -9,6 +9,13 @@ import { resolveAgentWorkspaceDir, resolveSessionAgentId, } from "../../agents/agent-scope.js"; +import { + isToolAllowedByPolicies, + resolveEffectiveToolPolicy, + resolveGroupToolPolicy, + resolveInheritedToolPolicyForSession, + resolveSubagentToolPolicyForSession, +} from "../../agents/agent-tools.policy.js"; import { selectAgentHarness } from "../../agents/harness/selection.js"; import { buildModelAliasIndex, @@ -16,13 +23,6 @@ import { resolveModelRefFromString, type ModelAliasIndex, } from "../../agents/model-selection.js"; -import { - isToolAllowedByPolicies, - resolveEffectiveToolPolicy, - resolveGroupToolPolicy, - resolveInheritedToolPolicyForSession, - resolveSubagentToolPolicyForSession, -} from "../../agents/pi-tools.policy.js"; import { isSubagentEnvelopeSession, resolveSubagentCapabilityStore, @@ -194,7 +194,9 @@ const getReplyFromConfigRuntimeLoader = createLazyImportLoader( ); const abortRuntimeLoader = createLazyImportLoader(() => import("./abort.runtime.js")); const ttsRuntimeLoader = createLazyImportLoader(() => import("../../tts/tts.runtime.js")); -const runtimePluginsLoader = createLazyImportLoader(() => import("./runtime-plugins.runtime.js")); +const runtimePluginsLoader = createLazyImportLoader( + () => import("../../plugins/runtime-plugins.runtime.js"), +); const replyMediaPathsRuntimeLoader = createLazyImportLoader( () => import("./reply-media-paths.runtime.js"), ); diff --git a/src/auto-reply/reply/export-html/template.js b/src/auto-reply/reply/export-html/template.js index 574221e93ea..a27a201ce32 100644 --- a/src/auto-reply/reply/export-html/template.js +++ b/src/auto-reply/reply/export-html/template.js @@ -20,7 +20,7 @@ // Parse URL parameters for deep linking: leafId and targetId // Check for injected params (when loaded in iframe via srcdoc) or use window.location - const injectedParams = document.querySelector('meta[name="pi-url-params"]'); + const injectedParams = document.querySelector('meta[name="openclaw-url-params"]'); const searchString = injectedParams ? injectedParams.content : window.location.search.substring(1); @@ -1241,7 +1241,7 @@ */ function buildShareUrl(entryId) { // Check for injected base URL (used when loaded in iframe via srcdoc) - const baseUrlMeta = document.querySelector('meta[name="pi-share-base-url"]'); + const baseUrlMeta = document.querySelector('meta[name="openclaw-share-base-url"]'); const baseUrl = baseUrlMeta ? baseUrlMeta.content : window.location.href.split("?")[0]; const url = new URL(window.location.href); diff --git a/src/auto-reply/reply/followup-delivery.ts b/src/auto-reply/reply/followup-delivery.ts index a2ecc1d192b..46f8c2333c5 100644 --- a/src/auto-reply/reply/followup-delivery.ts +++ b/src/auto-reply/reply/followup-delivery.ts @@ -1,4 +1,4 @@ -import type { MessagingToolSend } from "../../agents/pi-embedded-messaging.types.js"; +import type { MessagingToolSend } from "../../agents/embedded-agent-messaging.types.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { stripHeartbeatToken } from "../heartbeat.js"; import type { OriginatingChannelType } from "../templating.js"; diff --git a/src/auto-reply/reply/followup-runner.test.ts b/src/auto-reply/reply/followup-runner.test.ts index 33a3c159bb2..112b2fa20d8 100644 --- a/src/auto-reply/reply/followup-runner.test.ts +++ b/src/auto-reply/reply/followup-runner.test.ts @@ -11,10 +11,10 @@ import { } from "../../sessions/user-turn-transcript.js"; import type { FollowupRun, QueueSettings } from "./queue.js"; -const runEmbeddedPiAgentMock = vi.fn(); +const runEmbeddedAgentMock = vi.fn(); const runCliAgentMock = vi.fn(); const runWithModelFallbackMock = vi.fn(); -const compactEmbeddedPiSessionMock = vi.fn(); +const compactEmbeddedAgentSessionMock = vi.fn(); const routeReplyMock = vi.fn(); const isRoutableChannelMock = vi.fn(); const runPreflightCompactionIfNeededMock = vi.fn(); @@ -351,15 +351,15 @@ async function loadFreshFollowupRunnerModuleForTest() { })), resolveSessionLockMaxHoldFromTimeout: vi.fn(() => 1), })); - vi.doMock("../../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: vi.fn(async () => false), - compactEmbeddedPiSession: (params: unknown) => compactEmbeddedPiSessionMock(params), - isEmbeddedPiRunActive: vi.fn(() => false), - isEmbeddedPiRunStreaming: vi.fn(() => false), - queueEmbeddedPiMessage: vi.fn(async () => undefined), + vi.doMock("../../agents/embedded-agent.js", () => ({ + abortEmbeddedAgentRun: vi.fn(async () => false), + compactEmbeddedAgentSession: (params: unknown) => compactEmbeddedAgentSessionMock(params), + isEmbeddedAgentRunActive: vi.fn(() => false), + isEmbeddedAgentRunStreaming: vi.fn(() => false), + queueEmbeddedAgentMessage: vi.fn(async () => undefined), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, - runEmbeddedPiAgent: (params: unknown) => runEmbeddedPiAgentMock(params), - waitForEmbeddedPiRunEnd: vi.fn(async () => undefined), + runEmbeddedAgent: (params: unknown) => runEmbeddedAgentMock(params), + waitForEmbeddedAgentRunEnd: vi.fn(async () => undefined), })); vi.doMock("../../agents/cli-runner.js", () => ({ runCliAgent: (params: unknown) => runCliAgentMock(params), @@ -471,7 +471,7 @@ beforeAll(async () => { beforeEach(() => { replyRunTestingForTest?.resetReplyRunRegistry(); clearRuntimeConfigSnapshot?.(); - runEmbeddedPiAgentMock.mockReset(); + runEmbeddedAgentMock.mockReset(); runCliAgentMock.mockReset(); runWithModelFallbackMock.mockReset(); runWithModelFallbackMock.mockImplementation( @@ -489,7 +489,7 @@ beforeEach(() => { model: params.model, }), ); - compactEmbeddedPiSessionMock.mockReset(); + compactEmbeddedAgentSessionMock.mockReset(); runPreflightCompactionIfNeededMock.mockReset(); resolveCommandSecretRefsViaGatewayMock.mockReset(); resolveQueuedReplyExecutionConfigMock.mockReset(); @@ -600,7 +600,7 @@ describe("createFollowupRunner reply-lane admission", () => { resetTriggered: false, }); active.setPhase("preflight_compacting"); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: { agentMeta: { provider: "anthropic", model: "claude" } }, }); @@ -640,7 +640,7 @@ describe("createFollowupRunner reply-lane admission", () => { active.complete(); await pending; - const call = requireLastMockCallArg(runEmbeddedPiAgentMock, "run embedded pi agent"); + const call = requireLastMockCallArg(runEmbeddedAgentMock, "run embedded agent"); expect(call.sessionId).toBe("post-compact-session"); expect(call.sessionFile).toBe("/tmp/post-compact.jsonl"); }); @@ -658,7 +658,7 @@ function mockCompactionRun(params: { meta: Record; }; }) { - runEmbeddedPiAgentMock.mockImplementationOnce( + runEmbeddedAgentMock.mockImplementationOnce( async (args: { onAgentEvent?: (evt: { stream: string; data: Record }) => void; }) => { @@ -688,7 +688,7 @@ describe("createFollowupRunner auto fallback primary probes", () => { modelOverrideFallbackOriginModel: "claude", }; const sessionStore = { [sessionKey]: sessionEntry }; - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: { agentMeta: { provider: "anthropic", model: "claude" } }, }); @@ -718,7 +718,7 @@ describe("createFollowupRunner auto fallback primary probes", () => { }), ); - const call = requireLastMockCallArg(runEmbeddedPiAgentMock, "run embedded pi agent"); + const call = requireLastMockCallArg(runEmbeddedAgentMock, "run embedded agent"); expect(call.provider).toBe("anthropic"); expect(call.model).toBe("claude"); expect(sessionEntry.providerOverride).toBeUndefined(); @@ -752,7 +752,7 @@ describe("createFollowupRunner auto fallback primary probes", () => { const sessionStore = { [sessionKey]: sessionEntry }; const { markAutoFallbackPrimaryProbe } = await import("../../agents/agent-scope.js"); markAutoFallbackPrimaryProbe({ probe, sessionKey }); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: { agentMeta: { provider: "openai", model: "gpt-5.4" } }, }); @@ -787,7 +787,7 @@ describe("createFollowupRunner auto fallback primary probes", () => { }), ); - const call = requireLastMockCallArg(runEmbeddedPiAgentMock, "run embedded pi agent"); + const call = requireLastMockCallArg(runEmbeddedAgentMock, "run embedded agent"); expect(call.provider).toBe("openai"); expect(call.model).toBe("gpt-5.4"); expect(call.authProfileId).toBe("openai:fallback"); @@ -854,7 +854,7 @@ describe("createFollowupRunner runtime config", () => { }), ); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); expect(runCliAgentMock).toHaveBeenCalledTimes(1); const call = requireLastMockCallArg(runCliAgentMock, "run cli agent"); expect(call.provider).toBe("claude-cli"); @@ -958,7 +958,7 @@ describe("createFollowupRunner runtime config", () => { }, ); runCliAgentMock.mockRejectedValueOnce(new Error("cli failed")); - runEmbeddedPiAgentMock.mockImplementationOnce(async (params: { runId: string }) => { + runEmbeddedAgentMock.mockImplementationOnce(async (params: { runId: string }) => { realAgentEvents.emitAgentEvent({ runId: params.runId, stream: "lifecycle", @@ -1000,8 +1000,8 @@ describe("createFollowupRunner runtime config", () => { } expect(runCliAgentMock).toHaveBeenCalledTimes(1); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); - const embeddedCall = requireLastMockCallArg(runEmbeddedPiAgentMock, "run embedded pi agent"); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); + const embeddedCall = requireLastMockCallArg(runEmbeddedAgentMock, "run embedded agent"); expect(embeddedCall.suppressAssistantErrorPersistence).toBe(false); expect(lifecyclePhases).toEqual(["start", "start", "end"]); }); @@ -1034,7 +1034,7 @@ describe("createFollowupRunner runtime config", () => { }, }; setRuntimeConfigSnapshot(runtimeConfig, sourceConfig); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {}, }); @@ -1055,7 +1055,7 @@ describe("createFollowupRunner runtime config", () => { }), ); - const call = requireLastMockCallArg(runEmbeddedPiAgentMock, "run embedded pi agent"); + const call = requireLastMockCallArg(runEmbeddedAgentMock, "run embedded agent"); expect(call.config).toBe(runtimeConfig); }); @@ -1083,7 +1083,7 @@ describe("createFollowupRunner runtime config", () => { }), ); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); expect(onBlockReply).not.toHaveBeenCalled(); expect(typing.markRunComplete).toHaveBeenCalledTimes(1); expect(typing.markDispatchIdle).toHaveBeenCalledTimes(1); @@ -1091,7 +1091,7 @@ describe("createFollowupRunner runtime config", () => { it("passes the admitted reply abort signal into followup fallback and agent runs", async () => { const abortController = new AbortController(); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {}, }); @@ -1117,7 +1117,7 @@ describe("createFollowupRunner runtime config", () => { runWithModelFallbackMock, "run with model fallback", ); - const call = requireLastMockCallArg(runEmbeddedPiAgentMock, "run embedded pi agent"); + const call = requireLastMockCallArg(runEmbeddedAgentMock, "run embedded agent"); expect(fallbackCall.abortSignal).toBeInstanceOf(AbortSignal); expect(fallbackCall.abortSignal).not.toBe(abortController.signal); expect(call.abortSignal).toBe(fallbackCall.abortSignal); @@ -1126,7 +1126,7 @@ describe("createFollowupRunner runtime config", () => { it("does not inherit source abort signals for queued user followups", async () => { const sourceAbortController = new AbortController(); sourceAbortController.abort(); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {}, }); @@ -1152,7 +1152,7 @@ describe("createFollowupRunner runtime config", () => { runWithModelFallbackMock, "run with model fallback", ); - const call = requireLastMockCallArg(runEmbeddedPiAgentMock, "run embedded pi agent"); + const call = requireLastMockCallArg(runEmbeddedAgentMock, "run embedded agent"); expect(fallbackCall.abortSignal).toBeInstanceOf(AbortSignal); expect(fallbackCall.abortSignal).not.toBe(sourceAbortController.signal); expect(call.abortSignal).toBe(fallbackCall.abortSignal); @@ -1160,7 +1160,7 @@ describe("createFollowupRunner runtime config", () => { it("keeps queued delivery correlations active during followup agent runs", async () => { const events: string[] = []; - runEmbeddedPiAgentMock.mockImplementationOnce(async () => { + runEmbeddedAgentMock.mockImplementationOnce(async () => { events.push("run"); return { payloads: [], @@ -1226,7 +1226,7 @@ describe("createFollowupRunner runtime config", () => { targetStatesByPath: { "skills.entries.whisper.apiKey": "resolved_local" }, hadUnresolvedTargets: false, }); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {}, }); @@ -1248,12 +1248,12 @@ describe("createFollowupRunner runtime config", () => { expect(queued.run.config).toBe(runtimeConfig); expect(requireMockCallArg(runPreflightCompactionIfNeededMock, 0).cfg).toBe(runtimeConfig); - const call = requireLastMockCallArg(runEmbeddedPiAgentMock, "run embedded pi agent"); + const call = requireLastMockCallArg(runEmbeddedAgentMock, "run embedded agent"); expect(call.config).toBe(runtimeConfig); }); it("passes queued origin scope into queued execution-config resolution", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {}, }); @@ -1286,7 +1286,7 @@ describe("createFollowupRunner runtime config", () => { }); it("passes queued images into queued embedded followup runs", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {}, }); @@ -1309,7 +1309,7 @@ describe("createFollowupRunner runtime config", () => { }), ); - const call = requireLastMockCallArg(runEmbeddedPiAgentMock, "run embedded pi agent"); + const call = requireLastMockCallArg(runEmbeddedAgentMock, "run embedded agent"); expect(call.images).toBe(images); expect(call.imageOrder).toBe(imageOrder); }); @@ -1330,7 +1330,7 @@ describe("createFollowupRunner progress forwarding", () => { }, }); - runEmbeddedPiAgentMock.mockImplementationOnce( + runEmbeddedAgentMock.mockImplementationOnce( async (args: { onAgentEvent?: (evt: { stream: string; data: Record }) => Promise; onToolResult?: (payload: { text: string }) => Promise; @@ -1407,7 +1407,7 @@ describe("createFollowupRunner progress forwarding", () => { ); }); - runEmbeddedPiAgentMock.mockImplementationOnce( + runEmbeddedAgentMock.mockImplementationOnce( async (args: { onToolResult?: (payload: { text: string }) => Promise }) => { void args.onToolResult?.({ text: "🛠️ Exec: echo queued-progress" }); return { payloads: [{ text: "final reply" }], meta: { agentMeta: {} } }; @@ -1455,7 +1455,7 @@ describe("createFollowupRunner progress forwarding", () => { }, }); - runEmbeddedPiAgentMock.mockImplementationOnce( + runEmbeddedAgentMock.mockImplementationOnce( async (args: { onAgentEvent?: (evt: { stream: string; data: Record }) => Promise; onToolResult?: (payload: { text: string }) => Promise; @@ -1530,7 +1530,7 @@ describe("createFollowupRunner progress forwarding", () => { const onCompactionEnd = vi.fn(async () => {}); registerFollowupTestSessionStore(storePath, sessionStore); - runEmbeddedPiAgentMock.mockImplementationOnce( + runEmbeddedAgentMock.mockImplementationOnce( async (args: { onAgentEvent?: (evt: { stream: string; data: Record }) => Promise; shouldEmitToolResult?: () => boolean; @@ -1934,7 +1934,7 @@ describe("createFollowupRunner compaction", () => { const onBlockReply = vi.fn(async () => {}); registerFollowupTestSessionStore(storePath, sessionStore); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "final" }], meta: { agentMeta: { @@ -1989,7 +1989,7 @@ describe("createFollowupRunner compaction", () => { }; registerFollowupTestSessionStore(storePath, sessionStore); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "final" }], meta: { agentMeta: { @@ -2069,7 +2069,7 @@ describe("createFollowupRunner compaction", () => { }, }); - runEmbeddedPiAgentMock.mockImplementationOnce(async (args) => { + runEmbeddedAgentMock.mockImplementationOnce(async (args) => { args.onAgentEvent?.({ stream: "compaction", data: { phase: "end", willRetry: false, completed: false }, @@ -2133,7 +2133,7 @@ describe("createFollowupRunner compaction", () => { }; registerFollowupTestSessionStore(storePath, sessionStore); - compactEmbeddedPiSessionMock.mockResolvedValueOnce({ + compactEmbeddedAgentSessionMock.mockResolvedValueOnce({ ok: true, compacted: true, result: { @@ -2151,7 +2151,7 @@ describe("createFollowupRunner compaction", () => { sessionKey?: string; storePath?: string; }) => { - await compactEmbeddedPiSessionMock({ + await compactEmbeddedAgentSessionMock({ sessionFile: transcriptPath, workspaceDir, }); @@ -2187,15 +2187,13 @@ describe("createFollowupRunner compaction", () => { ); const embeddedCalls: Array<{ extraSystemPrompt?: string }> = []; - runEmbeddedPiAgentMock.mockImplementationOnce( - async (params: { extraSystemPrompt?: string }) => { - embeddedCalls.push({ extraSystemPrompt: params.extraSystemPrompt }); - return { - payloads: [{ text: "final" }], - meta: { agentMeta: { usage: { input: 1, output: 1 } } }, - }; - }, - ); + runEmbeddedAgentMock.mockImplementationOnce(async (params: { extraSystemPrompt?: string }) => { + embeddedCalls.push({ extraSystemPrompt: params.extraSystemPrompt }); + return { + payloads: [{ text: "final" }], + meta: { agentMeta: { usage: { input: 1, output: 1 } } }, + }; + }); const runner = createFollowupRunner({ opts: { onBlockReply: vi.fn(async () => {}) }, @@ -2218,7 +2216,7 @@ describe("createFollowupRunner compaction", () => { await runner(queued); - expect(compactEmbeddedPiSessionMock).toHaveBeenCalledOnce(); + expect(compactEmbeddedAgentSessionMock).toHaveBeenCalledOnce(); expect(embeddedCalls[0]?.extraSystemPrompt).toContain("Post-compaction context refresh"); expect(embeddedCalls[0]?.extraSystemPrompt).toContain("Read AGENTS.md before replying."); expect(sessionStore.main?.compactionCount).toBe(2); @@ -2227,7 +2225,7 @@ describe("createFollowupRunner compaction", () => { describe("createFollowupRunner bootstrap warning dedupe", () => { it("passes stored warning signature history to embedded followup runs", async () => { - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: {}, }); @@ -2278,7 +2276,7 @@ describe("createFollowupRunner bootstrap warning dedupe", () => { await runner(baseQueuedRun()); - const call = requireLastMockCallArg(runEmbeddedPiAgentMock, "run embedded pi agent"); + const call = requireLastMockCallArg(runEmbeddedAgentMock, "run embedded agent"); expect(call.allowGatewaySubagentBinding).toBe(true); expect(call.bootstrapPromptWarningSignaturesSeen).toEqual(["sig-a", "sig-b"]); expect(call.bootstrapPromptWarningSignature).toBe("sig-b"); @@ -2321,7 +2319,7 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { }>; }) { const onBlockReply = createAsyncReplySpy(); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: {}, ...params.agentResult, }); @@ -2407,7 +2405,7 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { }, }; const persistSpy = vi.spyOn(sessionRunAccounting, "persistRunSessionUsage"); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], meta: { agentMeta: { @@ -2452,7 +2450,7 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() }; const sessionStore: Record = { [sessionKey]: sessionEntry }; const persistSpy = vi.spyOn(sessionRunAccounting, "persistRunSessionUsage"); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], meta: { agentMeta: { @@ -2643,7 +2641,7 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { } as FollowupRun, }); - const runArg = requireMockCallArg(runEmbeddedPiAgentMock, 0); + const runArg = requireMockCallArg(runEmbeddedAgentMock, 0); expect(runArg.sourceReplyDeliveryMode).toBe("message_tool_only"); expect(runArg.forceMessageTool).toBe(true); expect(routeReplyMock).not.toHaveBeenCalled(); @@ -2697,7 +2695,7 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { it("suppresses exact NO_REPLY followups without origin or dispatcher delivery", async () => { const typing = createMockTypingController(); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: ` ${DELIVERY_NO_REPLY_RUNTIME_CONTRACT.silentText} ` }], meta: {}, }); @@ -2716,7 +2714,7 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { it("suppresses JSON NO_REPLY followups without origin or dispatcher delivery", async () => { const typing = createMockTypingController(); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: DELIVERY_NO_REPLY_RUNTIME_CONTRACT.jsonSilentText }], meta: {}, }); @@ -2820,7 +2818,7 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { describe("createFollowupRunner typing cleanup", () => { async function runTypingCase(agentResult: Record) { const typing = createMockTypingController(); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ meta: {}, ...agentResult, }); @@ -2853,7 +2851,7 @@ describe("createFollowupRunner typing cleanup", () => { it("calls both markRunComplete and markDispatchIdle on agent error", async () => { const typing = createMockTypingController(); - runEmbeddedPiAgentMock.mockRejectedValueOnce(new Error("agent exploded")); + runEmbeddedAgentMock.mockRejectedValueOnce(new Error("agent exploded")); const runner = createFollowupRunner({ opts: { onBlockReply: vi.fn(async () => {}) }, @@ -2870,7 +2868,7 @@ describe("createFollowupRunner typing cleanup", () => { it("calls both markRunComplete and markDispatchIdle on successful delivery", async () => { const typing = createMockTypingController(); const onBlockReply = vi.fn(async () => {}); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], meta: {}, }); @@ -2890,10 +2888,10 @@ describe("createFollowupRunner typing cleanup", () => { }); describe("createFollowupRunner agentDir forwarding", () => { - it("passes queued run agentDir to runEmbeddedPiAgent", async () => { - runEmbeddedPiAgentMock.mockClear(); + it("passes queued run agentDir to runEmbeddedAgent", async () => { + runEmbeddedAgentMock.mockClear(); const onBlockReply = vi.fn(async () => {}); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "hello world!" }], messagingToolSentTexts: ["different message"], meta: {}, @@ -2914,15 +2912,15 @@ describe("createFollowupRunner agentDir forwarding", () => { }, }); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); - const call = requireLastMockCallArg(runEmbeddedPiAgentMock, "run embedded pi agent"); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); + const call = requireLastMockCallArg(runEmbeddedAgentMock, "run embedded agent"); expect(call.agentDir).toBe(agentDir); }); }); describe("createFollowupRunner queued user message idempotency across fallback", () => { it("suppresses queued user message persistence after first fallback candidate persists it", async () => { - runEmbeddedPiAgentMock.mockClear(); + runEmbeddedAgentMock.mockClear(); runWithModelFallbackMock.mockReset(); runWithModelFallbackMock.mockImplementationOnce( async (params: { run: (provider: string, model: string) => Promise }) => { @@ -2934,7 +2932,7 @@ describe("createFollowupRunner queued user message idempotency across fallback", }; }, ); - runEmbeddedPiAgentMock.mockImplementationOnce( + runEmbeddedAgentMock.mockImplementationOnce( async (args: { onUserMessagePersisted?: (message: { role: "user"; @@ -2948,7 +2946,7 @@ describe("createFollowupRunner queued user message idempotency across fallback", throw new Error("upstream 500"); }, ); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: {}, }); @@ -2969,15 +2967,15 @@ describe("createFollowupRunner queued user message idempotency across fallback", }), ); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(2); - const firstAttempt = requireMockCallArg(runEmbeddedPiAgentMock, 0); - const secondAttempt = requireMockCallArg(runEmbeddedPiAgentMock, 1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(2); + const firstAttempt = requireMockCallArg(runEmbeddedAgentMock, 0); + const secondAttempt = requireMockCallArg(runEmbeddedAgentMock, 1); expect(firstAttempt.suppressNextUserMessagePersistence).toBe(false); expect(secondAttempt.suppressNextUserMessagePersistence).toBe(true); }); it("only persists assistant error stub on the first fallback candidate", async () => { - runEmbeddedPiAgentMock.mockClear(); + runEmbeddedAgentMock.mockClear(); runWithModelFallbackMock.mockReset(); runWithModelFallbackMock.mockImplementationOnce( async (params: { run: (provider: string, model: string) => Promise }) => { @@ -2990,7 +2988,7 @@ describe("createFollowupRunner queued user message idempotency across fallback", }; }, ); - runEmbeddedPiAgentMock.mockImplementationOnce( + runEmbeddedAgentMock.mockImplementationOnce( async (args: { onAssistantErrorMessagePersisted?: (message: { role: "assistant"; @@ -3006,8 +3004,8 @@ describe("createFollowupRunner queued user message idempotency across fallback", throw new Error("upstream 500"); }, ); - runEmbeddedPiAgentMock.mockRejectedValueOnce(new Error("upstream 500")); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockRejectedValueOnce(new Error("upstream 500")); + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: {}, }); @@ -3027,17 +3025,17 @@ describe("createFollowupRunner queued user message idempotency across fallback", }), ); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(3); - const firstAttempt = requireMockCallArg(runEmbeddedPiAgentMock, 0); - const secondAttempt = requireMockCallArg(runEmbeddedPiAgentMock, 1); - const thirdAttempt = requireMockCallArg(runEmbeddedPiAgentMock, 2); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(3); + const firstAttempt = requireMockCallArg(runEmbeddedAgentMock, 0); + const secondAttempt = requireMockCallArg(runEmbeddedAgentMock, 1); + const thirdAttempt = requireMockCallArg(runEmbeddedAgentMock, 2); expect(firstAttempt.suppressAssistantErrorPersistence).toBe(false); expect(secondAttempt.suppressAssistantErrorPersistence).toBe(true); expect(thirdAttempt.suppressAssistantErrorPersistence).toBe(true); }); it("does not suppress when no fallback candidate persisted the queued message", async () => { - runEmbeddedPiAgentMock.mockClear(); + runEmbeddedAgentMock.mockClear(); runWithModelFallbackMock.mockReset(); runWithModelFallbackMock.mockImplementationOnce( async (params: { run: (provider: string, model: string) => Promise }) => { @@ -3049,8 +3047,8 @@ describe("createFollowupRunner queued user message idempotency across fallback", }; }, ); - runEmbeddedPiAgentMock.mockRejectedValueOnce(new Error("upstream early")); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockRejectedValueOnce(new Error("upstream early")); + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: {}, }); @@ -3071,9 +3069,9 @@ describe("createFollowupRunner queued user message idempotency across fallback", }), ); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(2); - const firstAttempt = requireMockCallArg(runEmbeddedPiAgentMock, 0); - const secondAttempt = requireMockCallArg(runEmbeddedPiAgentMock, 1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(2); + const firstAttempt = requireMockCallArg(runEmbeddedAgentMock, 0); + const secondAttempt = requireMockCallArg(runEmbeddedAgentMock, 1); expect(firstAttempt.suppressNextUserMessagePersistence).toBe(false); expect(secondAttempt.suppressNextUserMessagePersistence).toBe(false); expect(secondAttempt.suppressAssistantErrorPersistence).toBe(false); diff --git a/src/auto-reply/reply/followup-runner.ts b/src/auto-reply/reply/followup-runner.ts index 24d2ded4442..2e82a63ea31 100644 --- a/src/auto-reply/reply/followup-runner.ts +++ b/src/auto-reply/reply/followup-runner.ts @@ -9,11 +9,11 @@ import { resolveBootstrapWarningSignaturesSeen } from "../../agents/bootstrap-bu import { getCliSessionBinding } from "../../agents/cli-session.js"; import { resolveContextTokensForModel } from "../../agents/context.js"; import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js"; +import { runEmbeddedAgent } from "../../agents/embedded-agent.js"; import { ensureSelectedAgentHarnessPlugin } from "../../agents/harness/runtime-plugin.js"; import { runWithModelFallback } from "../../agents/model-fallback.js"; import { resolveCliRuntimeExecutionProvider } from "../../agents/model-runtime-aliases.js"; import { isCliProvider } from "../../agents/model-selection-cli.js"; -import { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; import { buildAgentRuntimeDeliveryPlan, buildAgentRuntimeOutcomePlan, @@ -57,7 +57,7 @@ import { incrementRunCompactionCount, persistRunSessionUsage } from "./session-r import { createTypingSignaler } from "./typing-mode.js"; import type { TypingController } from "./typing.js"; -type EmbeddedAgentRunResult = Awaited>; +type EmbeddedAgentRunResult = Awaited>; type FollowupAgentEvent = { stream: string; data: Record }; @@ -532,7 +532,7 @@ export function createFollowupRunner(params: { }); } let autoCompactionCount = 0; - let runResult: Awaited>; + let runResult: Awaited>; let fallbackProvider = run.provider; let fallbackModel = run.model; activeSessionEntry = await runPreflightCompactionIfNeeded({ @@ -681,19 +681,17 @@ export function createFollowupRunner(params: { entry: activeSessionEntry, }); const cliExecutionProvider = - sessionRuntimeOverride === "pi" - ? provider - : ((sessionRuntimeOverride && isCliProvider(sessionRuntimeOverride, runtimeConfig) - ? sessionRuntimeOverride - : undefined) ?? - resolveCliRuntimeExecutionProvider({ - provider, - cfg: runtimeConfig, - agentId: run.agentId, - modelId: model, - authProfileId: selectedAuthProfile.authProfileId, - }) ?? - provider); + (sessionRuntimeOverride && isCliProvider(sessionRuntimeOverride, runtimeConfig) + ? sessionRuntimeOverride + : undefined) ?? + resolveCliRuntimeExecutionProvider({ + provider, + cfg: runtimeConfig, + agentId: run.agentId, + modelId: model, + authProfileId: selectedAuthProfile.authProfileId, + }) ?? + provider; let attemptCompactionCount = 0; const userTurnTranscriptRecorder = effectiveQueued.userTurnTranscriptRecorder ?? opts?.userTurnTranscriptRecorder; @@ -792,7 +790,7 @@ export function createFollowupRunner(params: { return result; } pendingDeferredCliTerminal = undefined; - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ allowGatewaySubagentBinding: true, replyOperation, sessionId: run.sessionId, diff --git a/src/auto-reply/reply/get-reply-inline-actions.ts b/src/auto-reply/reply/get-reply-inline-actions.ts index ec9c2964cff..b953b2730cf 100644 --- a/src/auto-reply/reply/get-reply-inline-actions.ts +++ b/src/auto-reply/reply/get-reply-inline-actions.ts @@ -1,5 +1,5 @@ import { collectTextContentBlocks } from "../../agents/content-blocks.js"; -import type { BlockReplyChunking } from "../../agents/pi-embedded-block-chunker.js"; +import type { BlockReplyChunking } from "../../agents/embedded-agent-block-chunker.js"; import type { SkillCommandSpec } from "../../agents/skills.js"; import { getChannelPlugin } from "../../channels/plugins/index.js"; import type { SessionEntry } from "../../config/sessions.js"; diff --git a/src/auto-reply/reply/get-reply-run.media-only.test.ts b/src/auto-reply/reply/get-reply-run.media-only.test.ts index f6a5d04e8ec..7bb9c5acc33 100644 --- a/src/auto-reply/reply/get-reply-run.media-only.test.ts +++ b/src/auto-reply/reply/get-reply-run.media-only.test.ts @@ -6,7 +6,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite import { clearActiveEmbeddedRun, setActiveEmbeddedRun, -} from "../../agents/pi-embedded-runner/runs.js"; +} from "../../agents/embedded-agent-runner/runs.js"; import type { SessionEntry } from "../../config/sessions.js"; import { createReplyOperation } from "./reply-run-registry.js"; @@ -14,14 +14,14 @@ vi.mock("../../agents/auth-profiles/session-override.js", () => ({ resolveSessionAuthProfileOverride: vi.fn().mockResolvedValue(undefined), })); -vi.mock("../../agents/pi-embedded.runtime.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), +vi.mock("../../agents/embedded-agent.runtime.js", () => ({ + abortEmbeddedAgentRun: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunActive: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunStreaming: vi.fn().mockReturnValue(false), resolveActiveEmbeddedRunSessionId: vi.fn().mockReturnValue(undefined), resolveActiveEmbeddedRunSessionIdBySessionFile: vi.fn().mockReturnValue(undefined), resolveEmbeddedSessionLane: vi.fn().mockReturnValue("session:session-key"), - waitForEmbeddedPiRunEnd: vi.fn().mockResolvedValue(true), + waitForEmbeddedAgentRunEnd: vi.fn().mockResolvedValue(true), })); vi.mock("../../config/sessions/group.js", () => ({ @@ -551,18 +551,18 @@ describe("runPreparedReply media-only handling", () => { "webchat", ] as const)("enables default same-turn steering for active %s runs", async (channel) => { const queueSettings = await import("./queue/settings-runtime.js"); - const piRuntime = await import("../../agents/pi-embedded.runtime.js"); + const embeddedAgentRuntime = await import("../../agents/embedded-agent.runtime.js"); vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "steer", debounceMs: 500, cap: 20, dropPolicy: "summarize", }); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId) + vi.mocked(embeddedAgentRuntime.resolveActiveEmbeddedRunSessionId) .mockReturnValueOnce("active-session") .mockReturnValueOnce("active-session"); - vi.mocked(piRuntime.isEmbeddedPiRunActive).mockReturnValueOnce(true); - vi.mocked(piRuntime.isEmbeddedPiRunStreaming).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunActive).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); const params = baseParams({ sessionKey: `agent:main:${channel}:direct:steer-smoke`, @@ -1272,12 +1272,14 @@ describe("runPreparedReply media-only handling", () => { }); it("treats reset-triggered followup mode as interrupt when the session lane is empty", async () => { const queueSettings = await import("./queue/settings-runtime.js"); - const piRuntime = await import("../../agents/pi-embedded.runtime.js"); + const embeddedAgentRuntime = await import("../../agents/embedded-agent.runtime.js"); const commandQueue = await import("../../process/command-queue.js"); vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "followup" }); vi.mocked(commandQueue.getQueueSize).mockReturnValueOnce(0); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId).mockReturnValue("session-active"); - vi.mocked(piRuntime.abortEmbeddedPiRun).mockReturnValue(true); + vi.mocked(embeddedAgentRuntime.resolveActiveEmbeddedRunSessionId).mockReturnValue( + "session-active", + ); + vi.mocked(embeddedAgentRuntime.abortEmbeddedAgentRun).mockReturnValue(true); const activeOperation = createReplyOperation({ sessionId: "session-active", sessionKey: "session-key", @@ -1295,7 +1297,7 @@ describe("runPreparedReply media-only handling", () => { expect(result).toEqual({ text: "ok" }); expect(commandQueue.clearCommandLane).toHaveBeenCalledWith("session:session-key"); - expect(piRuntime.abortEmbeddedPiRun).toHaveBeenCalledWith("session-active"); + expect(embeddedAgentRuntime.abortEmbeddedAgentRun).toHaveBeenCalledWith("session-active"); expect(activeOperation.result).toEqual({ kind: "aborted", code: "aborted_by_user" }); expect(vi.mocked(runReplyAgent)).toHaveBeenCalledOnce(); const call = requireRunReplyAgentCall(); @@ -1308,18 +1310,18 @@ describe("runPreparedReply media-only handling", () => { }); it("does not enable steering for active heartbeat runs", async () => { const queueSettings = await import("./queue/settings-runtime.js"); - const piRuntime = await import("../../agents/pi-embedded.runtime.js"); + const embeddedAgentRuntime = await import("../../agents/embedded-agent.runtime.js"); vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "followup", debounceMs: 500, cap: 20, dropPolicy: "summarize", }); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId) + vi.mocked(embeddedAgentRuntime.resolveActiveEmbeddedRunSessionId) .mockReturnValueOnce("active-session") .mockReturnValueOnce("active-session"); - vi.mocked(piRuntime.isEmbeddedPiRunActive).mockReturnValueOnce(true); - vi.mocked(piRuntime.isEmbeddedPiRunStreaming).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunActive).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); await runPreparedReply( baseParams({ @@ -1379,16 +1381,16 @@ describe("runPreparedReply media-only handling", () => { }); it("does not queue a run behind its provided pre-dispatch reply operation", async () => { - const piRuntime = await import("../../agents/pi-embedded.runtime.js"); + const embeddedAgentRuntime = await import("../../agents/embedded-agent.runtime.js"); const operation = createReplyOperation({ sessionId: "session-pre-dispatch-owner", sessionKey: "session-key", resetTriggered: false, }); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId).mockReturnValue( + vi.mocked(embeddedAgentRuntime.resolveActiveEmbeddedRunSessionId).mockReturnValue( "session-pre-dispatch-owner", ); - vi.mocked(piRuntime.isEmbeddedPiRunActive).mockReturnValue(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunActive).mockReturnValue(true); try { await expect( @@ -1403,17 +1405,19 @@ describe("runPreparedReply media-only handling", () => { const call = requireLastRunReplyAgentCall(); expect(call.replyOperation).toBe(operation); - expect(vi.mocked(piRuntime.isEmbeddedPiRunActive)).not.toHaveBeenCalled(); + expect(vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunActive)).not.toHaveBeenCalled(); } finally { operation.complete(); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId).mockReset().mockReturnValue(undefined); - vi.mocked(piRuntime.isEmbeddedPiRunActive).mockReset().mockReturnValue(false); + vi.mocked(embeddedAgentRuntime.resolveActiveEmbeddedRunSessionId) + .mockReset() + .mockReturnValue(undefined); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunActive).mockReset().mockReturnValue(false); } }); it("does not interrupt its provided pre-dispatch reply operation for reset turns", async () => { const queueSettings = await import("./queue/settings-runtime.js"); - const piRuntime = await import("../../agents/pi-embedded.runtime.js"); + const embeddedAgentRuntime = await import("../../agents/embedded-agent.runtime.js"); const commandQueue = await import("../../process/command-queue.js"); const operation = createReplyOperation({ sessionId: "session-reset-owner", @@ -1422,7 +1426,9 @@ describe("runPreparedReply media-only handling", () => { }); vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "followup" }); vi.mocked(commandQueue.getQueueSize).mockReturnValueOnce(0); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId).mockReturnValue("session-reset-owner"); + vi.mocked(embeddedAgentRuntime.resolveActiveEmbeddedRunSessionId).mockReturnValue( + "session-reset-owner", + ); try { await expect( @@ -1439,10 +1445,12 @@ describe("runPreparedReply media-only handling", () => { const call = requireLastRunReplyAgentCall(); expect(call.replyOperation).toBe(operation); expect(commandQueue.clearCommandLane).not.toHaveBeenCalled(); - expect(piRuntime.abortEmbeddedPiRun).not.toHaveBeenCalled(); + expect(embeddedAgentRuntime.abortEmbeddedAgentRun).not.toHaveBeenCalled(); } finally { operation.complete(); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId).mockReset().mockReturnValue(undefined); + vi.mocked(embeddedAgentRuntime.resolveActiveEmbeddedRunSessionId) + .mockReset() + .mockReturnValue(undefined); } }); @@ -1784,7 +1792,7 @@ describe("runPreparedReply media-only handling", () => { it("queues active room events as followups instead of steering fake prompts", async () => { const queueSettings = await import("./queue/settings-runtime.js"); - const piRuntime = await import("../../agents/pi-embedded.runtime.js"); + const embeddedAgentRuntime = await import("../../agents/embedded-agent.runtime.js"); const abortController = new AbortController(); vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "steer", @@ -1792,13 +1800,13 @@ describe("runPreparedReply media-only handling", () => { cap: 20, dropPolicy: "summarize", }); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId) + vi.mocked(embeddedAgentRuntime.resolveActiveEmbeddedRunSessionId) .mockReturnValueOnce("active-session") .mockReturnValueOnce("active-session"); - vi.mocked(piRuntime.isEmbeddedPiRunActive).mockReturnValueOnce(true); - vi.mocked(piRuntime.isEmbeddedPiRunStreaming).mockReturnValueOnce(true); - vi.mocked(piRuntime.abortEmbeddedPiRun).mockClear(); - vi.mocked(piRuntime.waitForEmbeddedPiRunEnd).mockClear(); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunActive).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.abortEmbeddedAgentRun).mockClear(); + vi.mocked(embeddedAgentRuntime.waitForEmbeddedAgentRunEnd).mockClear(); vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce("room context"); await runPreparedReply( @@ -1838,7 +1846,7 @@ describe("runPreparedReply media-only handling", () => { it("uses queued followup abort ownership instead of borrowed active-lane abort ownership", async () => { const queueSettings = await import("./queue/settings-runtime.js"); - const piRuntime = await import("../../agents/pi-embedded.runtime.js"); + const embeddedAgentRuntime = await import("../../agents/embedded-agent.runtime.js"); const activeLaneAbortController = new AbortController(); const sourceAbortController = new AbortController(); vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ @@ -1847,11 +1855,11 @@ describe("runPreparedReply media-only handling", () => { cap: 20, dropPolicy: "summarize", }); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId) + vi.mocked(embeddedAgentRuntime.resolveActiveEmbeddedRunSessionId) .mockReturnValueOnce("active-session") .mockReturnValueOnce("active-session"); - vi.mocked(piRuntime.isEmbeddedPiRunActive).mockReturnValueOnce(true); - vi.mocked(piRuntime.isEmbeddedPiRunStreaming).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunActive).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce("room context"); await runPreparedReply( @@ -1892,7 +1900,7 @@ describe("runPreparedReply media-only handling", () => { it("detaches queued user requests from superseded source abort signals", async () => { const queueSettings = await import("./queue/settings-runtime.js"); - const piRuntime = await import("../../agents/pi-embedded.runtime.js"); + const embeddedAgentRuntime = await import("../../agents/embedded-agent.runtime.js"); const abortController = new AbortController(); vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "collect", @@ -1900,11 +1908,11 @@ describe("runPreparedReply media-only handling", () => { cap: 20, dropPolicy: "summarize", }); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId) + vi.mocked(embeddedAgentRuntime.resolveActiveEmbeddedRunSessionId) .mockReturnValueOnce("active-session") .mockReturnValueOnce("active-session"); - vi.mocked(piRuntime.isEmbeddedPiRunActive).mockReturnValueOnce(true); - vi.mocked(piRuntime.isEmbeddedPiRunStreaming).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunActive).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce("user request context"); await runPreparedReply( @@ -1940,18 +1948,18 @@ describe("runPreparedReply media-only handling", () => { it("queues active room events instead of interrupting active user requests", async () => { const queueSettings = await import("./queue/settings-runtime.js"); - const piRuntime = await import("../../agents/pi-embedded.runtime.js"); + const embeddedAgentRuntime = await import("../../agents/embedded-agent.runtime.js"); vi.mocked(queueSettings.resolveQueueSettings).mockReturnValueOnce({ mode: "interrupt", debounceMs: 500, cap: 20, dropPolicy: "summarize", }); - vi.mocked(piRuntime.resolveActiveEmbeddedRunSessionId) + vi.mocked(embeddedAgentRuntime.resolveActiveEmbeddedRunSessionId) .mockReturnValueOnce("active-session") .mockReturnValueOnce("active-session"); - vi.mocked(piRuntime.isEmbeddedPiRunActive).mockReturnValueOnce(true); - vi.mocked(piRuntime.isEmbeddedPiRunStreaming).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunActive).mockReturnValueOnce(true); + vi.mocked(embeddedAgentRuntime.isEmbeddedAgentRunStreaming).mockReturnValueOnce(true); vi.mocked(buildInboundUserContextPrefix).mockReturnValueOnce("room context"); await runPreparedReply( @@ -1982,8 +1990,8 @@ describe("runPreparedReply media-only handling", () => { expect(call.shouldFollowup).toBe(true); expect(call.isActive).toBe(true); expect(call.resolvedQueue.mode).toBe("interrupt"); - expect(piRuntime.abortEmbeddedPiRun).not.toHaveBeenCalled(); - expect(piRuntime.waitForEmbeddedPiRunEnd).not.toHaveBeenCalled(); + expect(embeddedAgentRuntime.abortEmbeddedAgentRun).not.toHaveBeenCalled(); + expect(embeddedAgentRuntime.waitForEmbeddedAgentRunEnd).not.toHaveBeenCalled(); }); it("keeps room events tool-only when group replies are automatic", async () => { diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index be9d6153790..ea6008720e6 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -6,12 +6,12 @@ import { } from "../../agents/agent-scope.js"; import { resolveSessionAuthProfileOverride } from "../../agents/auth-profiles/session-override.js"; import type { ExecToolDefaults } from "../../agents/bash-tools.js"; +import { resolveEmbeddedFullAccessState } from "../../agents/embedded-agent-runner/sandbox-info.js"; +import type { EmbeddedFullAccessBlockedReason } from "../../agents/embedded-agent-runner/types.js"; import { resolveFastModeState } from "../../agents/fast-mode.js"; import { runAgentHarnessBeforeMessageWriteHook } from "../../agents/harness/hook-helpers.js"; import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js"; import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../../agents/openai-codex-routing.js"; -import { resolveEmbeddedFullAccessState } from "../../agents/pi-embedded-runner/sandbox-info.js"; -import type { EmbeddedFullAccessBlockedReason } from "../../agents/pi-embedded-runner/types.js"; import { normalizeProviderId } from "../../agents/provider-id.js"; import { resolveIngressWorkspaceOverrideForSpawnedRun } from "../../agents/spawned-context.js"; import type { SilentReplyPromptMode } from "../../agents/system-prompt.types.js"; @@ -304,8 +304,8 @@ export function buildExecOverridePromptHint(params: { .join("\n"); } -const piEmbeddedRuntimeLoader = createLazyImportLoader( - () => import("../../agents/pi-embedded.runtime.js"), +const embeddedAgentRuntimeLoader = createLazyImportLoader( + () => import("../../agents/embedded-agent.runtime.js"), ); const agentRunnerRuntimeLoader = createLazyImportLoader(() => import("./agent-runner.runtime.js")); const sessionUpdatesRuntimeLoader = createLazyImportLoader( @@ -315,8 +315,8 @@ const sessionStoreRuntimeLoader = createLazyImportLoader( () => import("../../config/sessions/store.runtime.js"), ); -function loadPiEmbeddedRuntime() { - return piEmbeddedRuntimeLoader.load(); +function loadEmbeddedAgentRuntime() { + return embeddedAgentRuntimeLoader.load(); } function loadAgentRunnerRuntime() { @@ -948,14 +948,14 @@ export async function runPreparedReply( inlineMode: perMessageQueueMode, inlineOptions: perMessageQueueOptions, }); - const piRuntime = useFastReplyRuntime + const embeddedAgentRuntime = useFastReplyRuntime ? null - : await traceRunPhase("reply.load_pi_runtime", () => loadPiEmbeddedRuntime()); + : await traceRunPhase("reply.load_embedded_agent_runtime", () => loadEmbeddedAgentRuntime()); const resolveActiveEmbeddedSessionId = (sessionFile = preparedSessionState.sessionFile) => - piRuntime?.resolveActiveEmbeddedRunSessionId(sessionKey) ?? - piRuntime?.resolveActiveEmbeddedRunSessionIdBySessionFile?.(sessionFile); - const sessionLaneKey = piRuntime - ? piRuntime.resolveEmbeddedSessionLane(sessionKey ?? sessionIdFinal) + embeddedAgentRuntime?.resolveActiveEmbeddedRunSessionId(sessionKey) ?? + embeddedAgentRuntime?.resolveActiveEmbeddedRunSessionIdBySessionFile?.(sessionFile); + const sessionLaneKey = embeddedAgentRuntime + ? embeddedAgentRuntime.resolveEmbeddedSessionLane(sessionKey ?? sessionIdFinal) : undefined; const laneSize = sessionLaneKey ? getQueueSize(sessionLaneKey) : 0; const activeRunQueueMode = effectiveResetTriggered ? "interrupt" : resolvedQueue.mode; @@ -972,7 +972,7 @@ export async function runPreparedReply( (laneSize > 0 || activeSessionIdForInterrupt) ) { const cleared = clearCommandLane(sessionLaneKey); - const aborted = piRuntime?.abortEmbeddedPiRun( + const aborted = embeddedAgentRuntime?.abortEmbeddedAgentRun( activeSessionIdForInterrupt ?? preparedSessionState.sessionId, ); logVerbose(`Interrupting ${sessionLaneKey} (cleared ${cleared}, aborted=${aborted})`); @@ -1057,7 +1057,7 @@ export async function runPreparedReply( const replyOperationActiveSessionId = resolveActiveReplyOperationSessionId(); const activeSessionId = embeddedActiveSessionId ?? replyOperationActiveSessionId ?? preparedSessionState.sessionId; - if (!activeSessionId || (!piRuntime && !replyOperationActiveSessionId)) { + if (!activeSessionId || (!embeddedAgentRuntime && !replyOperationActiveSessionId)) { return { activeSessionId: undefined, isActive: false, isStreaming: false }; } if (isOwnPreDispatchOperationSession(activeSessionId)) { @@ -1070,11 +1070,11 @@ export async function runPreparedReply( activeSessionId, isActive: (embeddedActiveSessionId != null && - (piRuntime?.isEmbeddedPiRunActive(embeddedActiveSessionId) ?? false)) || + (embeddedAgentRuntime?.isEmbeddedAgentRunActive(embeddedActiveSessionId) ?? false)) || replyOperationActive, isStreaming: (embeddedActiveSessionId != null && - (piRuntime?.isEmbeddedPiRunStreaming(embeddedActiveSessionId) ?? false)) || + (embeddedAgentRuntime?.isEmbeddedAgentRunStreaming(embeddedActiveSessionId) ?? false)) || (replyOperationActiveSessionId != null && isReplyRunStreamingForSessionId(replyOperationActiveSessionId)), }; @@ -1104,14 +1104,16 @@ export async function runPreparedReply( sessionKey, sessionId: sessionIdFinal, abortActiveRun: (activeRunSessionId) => { - const embeddedAborted = piRuntime?.abortEmbeddedPiRun(activeRunSessionId) ?? false; + const embeddedAborted = + embeddedAgentRuntime?.abortEmbeddedAgentRun(activeRunSessionId) ?? false; const replyOperationAborted = abortReplyRunBySessionId(activeRunSessionId); return embeddedAborted || replyOperationAborted; }, waitForActiveRunEnd: (activeRunSessionId) => isReplyRunActiveForSessionId(activeRunSessionId) ? waitForReplyRunEndBySessionId(activeRunSessionId, REPLY_RUN_IDLE_SETTLE_TIMEOUT_MS) - : (piRuntime?.waitForEmbeddedPiRunEnd(activeRunSessionId) ?? Promise.resolve(undefined)), + : (embeddedAgentRuntime?.waitForEmbeddedAgentRunEnd(activeRunSessionId) ?? + Promise.resolve(undefined)), refreshPreparedState: async () => { preparedSessionState = resolvePreparedSessionState(); ({ authProfileId, authProfileIdSource } = await resolveRuntimeAuthProfile()); @@ -1323,7 +1325,7 @@ export async function runPreparedReply( const latestActiveSessionId = resolveActiveEmbeddedSessionId(latestSessionState.sessionFile) ?? latestSessionState.sessionId; - return piRuntime?.isEmbeddedPiRunActive(latestActiveSessionId) ?? false; + return embeddedAgentRuntime?.isEmbeddedAgentRunActive(latestActiveSessionId) ?? false; }, isStreaming, opts, diff --git a/src/auto-reply/reply/get-reply.fast-path.runtime.test.ts b/src/auto-reply/reply/get-reply.fast-path.runtime.test.ts index 2780291aae5..185eb97d381 100644 --- a/src/auto-reply/reply/get-reply.fast-path.runtime.test.ts +++ b/src/auto-reply/reply/get-reply.fast-path.runtime.test.ts @@ -34,7 +34,7 @@ describe("getReplyFromConfig fast-path runtime", () => { it("keeps old-style runtime tests fast with marked temp-home configs", async () => { await withTempHome(async (home) => { let seenPrompt: string | undefined; - agentMocks.runEmbeddedPiAgent.mockImplementation(async (params) => { + agentMocks.runEmbeddedAgent.mockImplementation(async (params) => { seenPrompt = params.prompt; return makeEmbeddedTextResult("ok"); }); @@ -67,7 +67,7 @@ describe("getReplyFromConfig fast-path runtime", () => { it("routes structured native command turns through the target session before legacy sync", async () => { await withTempHome(async (home) => { - agentMocks.runEmbeddedPiAgent.mockResolvedValue(makeEmbeddedTextResult("ok")); + agentMocks.runEmbeddedAgent.mockResolvedValue(makeEmbeddedTextResult("ok")); await getReplyFromConfig( { @@ -90,7 +90,7 @@ describe("getReplyFromConfig fast-path runtime", () => { makeReplyConfig(home) as OpenClawConfig, ); - expect(agentMocks.runEmbeddedPiAgent).toHaveBeenCalledWith( + expect(agentMocks.runEmbeddedAgent).toHaveBeenCalledWith( expect.objectContaining({ sessionKey: "agent:main:telegram:direct:target", }), @@ -100,7 +100,7 @@ describe("getReplyFromConfig fast-path runtime", () => { it("ignores stale native legacy source for structured normal turns before routing", async () => { await withTempHome(async (home) => { - agentMocks.runEmbeddedPiAgent.mockResolvedValue(makeEmbeddedTextResult("ok")); + agentMocks.runEmbeddedAgent.mockResolvedValue(makeEmbeddedTextResult("ok")); await getReplyFromConfig( { @@ -124,7 +124,7 @@ describe("getReplyFromConfig fast-path runtime", () => { makeReplyConfig(home) as OpenClawConfig, ); - expect(agentMocks.runEmbeddedPiAgent).toHaveBeenCalledWith( + expect(agentMocks.runEmbeddedAgent).toHaveBeenCalledWith( expect.objectContaining({ sessionKey: "agent:main:telegram:direct:source", }), diff --git a/src/auto-reply/reply/model-selection.test.ts b/src/auto-reply/reply/model-selection.test.ts index e98eb44b4fc..c920b587699 100644 --- a/src/auto-reply/reply/model-selection.test.ts +++ b/src/auto-reply/reply/model-selection.test.ts @@ -381,7 +381,7 @@ describe("createModelSelectionState catalog loading", () => { expect(loadModelCatalog).toHaveBeenCalledOnce(); }); - it("preserves OpenAI API-key session auth when model policy explicitly pins PI", async () => { + it("preserves OpenAI API-key session auth when model policy explicitly pins OpenClaw", async () => { authProfileStoreMock.store = { version: 1, profiles: { @@ -401,7 +401,7 @@ describe("createModelSelectionState catalog loading", () => { providers: { openai: { baseUrl: "https://api.openai.com/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, diff --git a/src/auto-reply/reply/normalize-reply.ts b/src/auto-reply/reply/normalize-reply.ts index caf9f858e38..ed5405dd1b1 100644 --- a/src/auto-reply/reply/normalize-reply.ts +++ b/src/auto-reply/reply/normalize-reply.ts @@ -1,4 +1,4 @@ -import { sanitizeUserFacingText } from "../../agents/pi-embedded-helpers/sanitize-user-facing-text.js"; +import { sanitizeUserFacingText } from "../../agents/embedded-agent-helpers/sanitize-user-facing-text.js"; import { hasReplyPayloadContent } from "../../interactive/payload.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; import { stripHeartbeatToken } from "../heartbeat.js"; diff --git a/src/auto-reply/reply/prompt-prelude.ts b/src/auto-reply/reply/prompt-prelude.ts index 5c1fc98f52b..afccdac31ac 100644 --- a/src/auto-reply/reply/prompt-prelude.ts +++ b/src/auto-reply/reply/prompt-prelude.ts @@ -1,4 +1,4 @@ -import type { CurrentInboundPromptContext } from "../../agents/pi-embedded-runner/run/params.js"; +import type { CurrentInboundPromptContext } from "../../agents/embedded-agent-runner/run/params.js"; import type { InboundEventKind } from "../../channels/inbound-event/kind.js"; import { annotateInterSessionPromptText } from "../../sessions/input-provenance.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; diff --git a/src/auto-reply/reply/queue/cleanup.test.ts b/src/auto-reply/reply/queue/cleanup.test.ts index 39b8dae683b..bbeee422a4a 100644 --- a/src/auto-reply/reply/queue/cleanup.test.ts +++ b/src/auto-reply/reply/queue/cleanup.test.ts @@ -22,7 +22,7 @@ vi.mock("../../../process/command-queue.js", () => ({ clearCommandLane: commandQueueMocks.clearCommandLane, })); -vi.mock("../../../agents/pi-embedded-runner/lanes.js", () => ({ +vi.mock("../../../agents/embedded-agent-runner/lanes.js", () => ({ resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, })); diff --git a/src/auto-reply/reply/queue/cleanup.ts b/src/auto-reply/reply/queue/cleanup.ts index beef22c7a21..5363a6b04ba 100644 --- a/src/auto-reply/reply/queue/cleanup.ts +++ b/src/auto-reply/reply/queue/cleanup.ts @@ -1,4 +1,4 @@ -import { resolveEmbeddedSessionLane } from "../../../agents/pi-embedded-runner/lanes.js"; +import { resolveEmbeddedSessionLane } from "../../../agents/embedded-agent-runner/lanes.js"; import { clearCommandLane } from "../../../process/command-queue.js"; import { normalizeOptionalString } from "../../../shared/string-coerce.js"; import { clearFollowupDrainCallback } from "./drain.js"; diff --git a/src/auto-reply/reply/queue/types.ts b/src/auto-reply/reply/queue/types.ts index 58ccb0dc961..06ce950ceaf 100644 --- a/src/auto-reply/reply/queue/types.ts +++ b/src/auto-reply/reply/queue/types.ts @@ -1,6 +1,6 @@ import type { AutoFallbackPrimaryProbe } from "../../../agents/agent-scope.js"; import type { ExecToolDefaults } from "../../../agents/bash-tools.js"; -import type { CurrentInboundPromptContext } from "../../../agents/pi-embedded-runner/run/params.js"; +import type { CurrentInboundPromptContext } from "../../../agents/embedded-agent-runner/run/params.js"; import type { SkillSnapshot } from "../../../agents/skills.js"; import type { SilentReplyPromptMode } from "../../../agents/system-prompt.types.js"; import type { InboundEventKind } from "../../../channels/inbound-event/kind.js"; diff --git a/src/auto-reply/reply/reply-payloads-dedupe.ts b/src/auto-reply/reply/reply-payloads-dedupe.ts index 2d33c4ef44e..06be608958a 100644 --- a/src/auto-reply/reply/reply-payloads-dedupe.ts +++ b/src/auto-reply/reply/reply-payloads-dedupe.ts @@ -1,5 +1,5 @@ -import { isMessagingToolDuplicate } from "../../agents/pi-embedded-helpers.js"; -import type { MessagingToolSend } from "../../agents/pi-embedded-messaging.types.js"; +import { isMessagingToolDuplicate } from "../../agents/embedded-agent-helpers.js"; +import type { MessagingToolSend } from "../../agents/embedded-agent-messaging.types.js"; import { getChannelPlugin } from "../../channels/plugins/index.js"; import { getLoadedChannelPluginForRead } from "../../channels/plugins/registry-loaded-read.js"; import { normalizeAnyChannelId } from "../../channels/registry.js"; diff --git a/src/auto-reply/reply/runtime-plugins.runtime.ts b/src/auto-reply/reply/runtime-plugins.runtime.ts deleted file mode 100644 index 9a7872300f9..00000000000 --- a/src/auto-reply/reply/runtime-plugins.runtime.ts +++ /dev/null @@ -1 +0,0 @@ -export { ensureRuntimePluginsLoaded } from "../../agents/runtime-plugins.js"; diff --git a/src/auto-reply/reply/session-fork.runtime.ts b/src/auto-reply/reply/session-fork.runtime.ts index 14375aaae11..ffc5fbbc518 100644 --- a/src/auto-reply/reply/session-fork.runtime.ts +++ b/src/auto-reply/reply/session-fork.runtime.ts @@ -6,9 +6,9 @@ import { migrateSessionEntries, parseSessionEntries, type FileEntry, - type SessionEntry as PiSessionEntry, + type SessionEntry as AgentSessionEntry, type SessionHeader, -} from "@earendil-works/pi-coding-agent"; +} from "../../agents/sessions/index.js"; import { derivePromptTokens } from "../../agents/usage.js"; import { resolveSessionFilePath, @@ -25,7 +25,7 @@ type ForkSourceTranscript = { cwd: string; sessionDir: string; leafId: string | null; - branchEntries: PiSessionEntry[]; + branchEntries: AgentSessionEntry[]; labelsToWrite: Array<{ targetId: string; label: string; timestamp: string }>; }; @@ -106,7 +106,7 @@ export async function resolveParentForkTokenCountRuntime(params: { return maxPositiveTokenCount(cachedTokens, byteEstimateTokens); } -function isSessionEntry(entry: FileEntry): entry is PiSessionEntry { +function isSessionEntry(entry: FileEntry): entry is AgentSessionEntry { return ( entry.type !== "session" && typeof (entry as { id?: unknown }).id === "string" && @@ -115,15 +115,15 @@ function isSessionEntry(entry: FileEntry): entry is PiSessionEntry { ); } -function buildEntryIndex(entries: PiSessionEntry[]): Map { +function buildEntryIndex(entries: AgentSessionEntry[]): Map { return new Map(entries.map((entry) => [entry.id, entry])); } function readBranch(params: { - byId: Map; + byId: Map; leafId: string | null; -}): PiSessionEntry[] { - const branchEntries: PiSessionEntry[] = []; +}): AgentSessionEntry[] { + const branchEntries: AgentSessionEntry[] = []; let current = params.leafId ? params.byId.get(params.leafId) : undefined; while (current) { branchEntries.unshift(current); @@ -146,7 +146,7 @@ function generateEntryId(existingIds: Set): string { } function collectBranchLabels(params: { - allEntries: PiSessionEntry[]; + allEntries: AgentSessionEntry[]; pathEntryIds: Set; }): Array<{ targetId: string; label: string; timestamp: string }> { const labelsToWrite: Array<{ targetId: string; label: string; timestamp: string }> = []; @@ -195,9 +195,9 @@ function buildBranchLabelEntries(params: { labelsToWrite: Array<{ targetId: string; label: string; timestamp: string }>; pathEntryIds: Set; lastEntryId: string | null; -}): PiSessionEntry[] { +}): AgentSessionEntry[] { let parentId = params.lastEntryId; - const labelEntries: PiSessionEntry[] = []; + const labelEntries: AgentSessionEntry[] = []; for (const { targetId, label, timestamp } of params.labelsToWrite) { const labelEntry = { type: "label", @@ -206,7 +206,7 @@ function buildBranchLabelEntries(params: { timestamp, targetId, label, - } satisfies PiSessionEntry; + } satisfies AgentSessionEntry; params.pathEntryIds.add(labelEntry.id); labelEntries.push(labelEntry); parentId = labelEntry.id; diff --git a/src/auto-reply/reply/session-hooks-context.test.ts b/src/auto-reply/reply/session-hooks-context.test.ts index 0154bf6da30..42d0e85502a 100644 --- a/src/auto-reply/reply/session-hooks-context.test.ts +++ b/src/auto-reply/reply/session-hooks-context.test.ts @@ -31,7 +31,7 @@ vi.mock("../../agents/harness/registry.js", () => ({ resetRegisteredAgentHarnessSessions: sessionCleanupMocks.resetRegisteredAgentHarnessSessions, })); -vi.mock("../../agents/pi-bundle-mcp-tools.js", () => ({ +vi.mock("../../agents/agent-bundle-mcp-tools.js", () => ({ retireSessionMcpRuntime: sessionCleanupMocks.retireSessionMcpRuntime, })); diff --git a/src/auto-reply/reply/session-transcript-replay.ts b/src/auto-reply/reply/session-transcript-replay.ts index b44a501abe7..b99b22fc466 100644 --- a/src/auto-reply/reply/session-transcript-replay.ts +++ b/src/auto-reply/reply/session-transcript-replay.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import fsp from "node:fs/promises"; import path from "node:path"; -import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent"; +import { CURRENT_SESSION_VERSION } from "../../agents/sessions/index.js"; /** Tail kept so DM continuity survives silent session rotations. */ export const DEFAULT_REPLAY_MAX_MESSAGES = 6; diff --git a/src/auto-reply/reply/session.test.ts b/src/auto-reply/reply/session.test.ts index 36b7d2219b9..b9e4533c6dc 100644 --- a/src/auto-reply/reply/session.test.ts +++ b/src/auto-reply/reply/session.test.ts @@ -2,11 +2,11 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import * as bootstrapCache from "../../agents/bootstrap-cache.js"; import { testing as sessionMcpTesting, getOrCreateSessionMcpRuntime, -} from "../../agents/pi-bundle-mcp-tools.js"; +} from "../../agents/agent-bundle-mcp-tools.js"; +import * as bootstrapCache from "../../agents/bootstrap-cache.js"; import type { OpenClawConfig } from "../../config/config.js"; import type { SessionEntry } from "../../config/sessions.js"; import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.ts"; diff --git a/src/auto-reply/reply/session.ts b/src/auto-reply/reply/session.ts index 9fe59f27401..7132c32baff 100644 --- a/src/auto-reply/reply/session.ts +++ b/src/auto-reply/reply/session.ts @@ -1,10 +1,10 @@ import crypto from "node:crypto"; import path from "node:path"; +import { retireSessionMcpRuntime } from "../../agents/agent-bundle-mcp-tools.js"; import { resolveSessionAgentId } from "../../agents/agent-scope.js"; import { clearBootstrapSnapshotOnSessionRollover } from "../../agents/bootstrap-cache.js"; import { getCliSessionBinding } from "../../agents/cli-session.js"; import { resetRegisteredAgentHarnessSessions } from "../../agents/harness/registry.js"; -import { retireSessionMcpRuntime } from "../../agents/pi-bundle-mcp-tools.js"; import { normalizeChatType } from "../../channels/chat-type.js"; import { resolveGroupSessionKey } from "../../config/sessions/group.js"; import { resolveSessionLifecycleTimestamps } from "../../config/sessions/lifecycle.js"; diff --git a/src/auto-reply/reply/skill-tool-dispatch.runtime.ts b/src/auto-reply/reply/skill-tool-dispatch.runtime.ts index 7043f315c3f..62237b0cde9 100644 --- a/src/auto-reply/reply/skill-tool-dispatch.runtime.ts +++ b/src/auto-reply/reply/skill-tool-dispatch.runtime.ts @@ -1,11 +1,11 @@ -import { createOpenClawTools } from "../../agents/openclaw-tools.runtime.js"; import { resolveEffectiveToolPolicy, resolveGroupToolPolicy, resolveInheritedToolPolicyForSession, resolveSubagentToolPolicyForSession, -} from "../../agents/pi-tools.policy.js"; -import type { AnyAgentTool } from "../../agents/pi-tools.types.js"; +} from "../../agents/agent-tools.policy.js"; +import type { AnyAgentTool } from "../../agents/agent-tools.types.js"; +import { createOpenClawTools } from "../../agents/openclaw-tools.runtime.js"; import { resolveSandboxRuntimeStatus } from "../../agents/sandbox/runtime-status.js"; import { resolveSenderToolPolicy } from "../../agents/sender-tool-policy.js"; import type { SkillCommandSpec } from "../../agents/skills.js"; diff --git a/src/auto-reply/status.test.ts b/src/auto-reply/status.test.ts index 323f573746e..70dad296398 100644 --- a/src/auto-reply/status.test.ts +++ b/src/auto-reply/status.test.ts @@ -49,7 +49,7 @@ describe("buildStatusMessage", () => { apiKey: "test-key", models: [ { - id: "pi:opus", + id: "test:opus", cost: { input: 1, output: 1, @@ -63,7 +63,7 @@ describe("buildStatusMessage", () => { }, } as unknown as OpenClawConfig, agent: { - model: "anthropic/pi:opus", + model: "anthropic/test:opus", contextTokens: 32_000, }, sessionEntry: { @@ -88,7 +88,7 @@ describe("buildStatusMessage", () => { const normalized = normalizeTestText(text); expect(normalized).toContain("OpenClaw"); - expect(normalized).toContain("Model: anthropic/pi:opus"); + expect(normalized).toContain("Model: anthropic/test:opus"); expect(normalized).toContain("api-key"); expect(normalized).toContain("Tokens: 1.2k in / 800 out"); expect(normalized).toContain("Cost: $0.0020"); @@ -97,7 +97,7 @@ describe("buildStatusMessage", () => { expect(normalized).toContain("Session: agent:main:main"); expect(normalized).toContain("updated 10m ago"); expect(normalized).toContain("Execution: direct"); - expect(normalized).toContain("Runtime: OpenClaw Pi Default"); + expect(normalized).toContain("Runtime: OpenClaw Default"); expect(normalized).not.toContain("Runner:"); expect(normalized).toContain("Think: medium"); expect(normalized).not.toContain("verbose"); @@ -159,7 +159,7 @@ describe("buildStatusMessage", () => { it("does not render stale totalTokens as current context usage", () => { const text = buildStatusMessage({ agent: { - model: "anthropic/pi:opus", + model: "anthropic/test:opus", contextTokens: 1_000_000, }, sessionEntry: { @@ -454,7 +454,7 @@ describe("buildStatusMessage", () => { it("falls back to sessionEntry levels when resolved levels are not passed", () => { const text = buildStatusMessage({ agent: { - model: "anthropic/pi:opus", + model: "anthropic/test:opus", }, sessionEntry: { sessionId: "abc", @@ -477,7 +477,7 @@ describe("buildStatusMessage", () => { const visible = normalizeTestText( buildStatusMessage({ agent: { - model: "anthropic/pi:opus", + model: "anthropic/test:opus", }, sessionEntry: { sessionId: "abc", @@ -497,7 +497,7 @@ describe("buildStatusMessage", () => { const hidden = normalizeTestText( buildStatusMessage({ agent: { - model: "anthropic/pi:opus", + model: "anthropic/test:opus", }, sessionEntry: { sessionId: "abc", @@ -523,7 +523,7 @@ describe("buildStatusMessage", () => { const visible = normalizeTestText( buildStatusMessage({ agent: { - model: "anthropic/pi:opus", + model: "anthropic/test:opus", }, sessionEntry: { sessionId: "abc", @@ -550,7 +550,7 @@ describe("buildStatusMessage", () => { const hidden = normalizeTestText( buildStatusMessage({ agent: { - model: "anthropic/pi:opus", + model: "anthropic/test:opus", }, sessionEntry: { sessionId: "abc", @@ -567,7 +567,7 @@ describe("buildStatusMessage", () => { const visible = normalizeTestText( buildStatusMessage({ agent: { - model: "anthropic/pi:opus", + model: "anthropic/test:opus", }, sessionEntry: { sessionId: "abc", @@ -592,7 +592,7 @@ describe("buildStatusMessage", () => { const visible = normalizeTestText( buildStatusMessage({ agent: { - model: "anthropic/pi:opus", + model: "anthropic/test:opus", }, sessionEntry: { sessionId: "abc", @@ -650,25 +650,25 @@ describe("buildStatusMessage", () => { expect(normalized).not.toContain("· codex"); }); - it("shows the default PI harness as the model runtime", () => { + it("shows the default OpenClaw harness as the model runtime", () => { const text = buildStatusMessage({ agent: { model: "openai/gpt-5.4", }, sessionEntry: { - sessionId: "pi-harness", + sessionId: "openclaw-harness", updatedAt: 0, fastMode: true, }, sessionKey: "agent:main:main", queue: { mode: "collect", depth: 0 }, - resolvedHarness: "pi", + resolvedHarness: "openclaw", }); const normalized = normalizeTestText(text); expect(normalized).toContain("Fast"); - expect(normalized).toContain("Runtime: OpenClaw Pi Default"); - expect(normalized).not.toContain("· pi"); + expect(normalized).toContain("Runtime: OpenClaw Default"); + expect(normalized).not.toContain("· openclaw"); }); it("shows fast mode when disabled", () => { diff --git a/src/channels/plugins/message-action-dispatch.ts b/src/channels/plugins/message-action-dispatch.ts index aa73da4700a..d30b8ab117c 100644 --- a/src/channels/plugins/message-action-dispatch.ts +++ b/src/channels/plugins/message-action-dispatch.ts @@ -1,4 +1,4 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; +import type { AgentToolResult } from "../../agents/runtime/index.js"; import { getChannelPlugin } from "./index.js"; import type { ChannelMessageActionContext } from "./types.public.js"; diff --git a/src/channels/plugins/types.core.ts b/src/channels/plugins/types.core.ts index 91a7e50ab8c..dfc1ef0eeaa 100644 --- a/src/channels/plugins/types.core.ts +++ b/src/channels/plugins/types.core.ts @@ -1,5 +1,5 @@ -import type { AgentTool, AgentToolResult } from "@earendil-works/pi-agent-core"; import type { TSchema } from "typebox"; +import type { AgentTool, AgentToolResult } from "../../agents/runtime/index.js"; import type { ReplyPayload } from "../../auto-reply/reply-payload.js"; import type { MsgContext } from "../../auto-reply/templating.js"; import type { MarkdownTableMode } from "../../config/types.base.js"; @@ -25,7 +25,7 @@ export type ChannelExposure = { export type ChannelOutboundTargetMode = "explicit" | "implicit" | "heartbeat"; /** Agent tool registered by a channel plugin. */ -export type ChannelAgentTool = AgentTool; +export type ChannelAgentTool = AgentTool; /** Lazy agent-tool factory used when tool availability depends on config. */ export type ChannelAgentToolFactory = (params: { cfg?: OpenClawConfig }) => ChannelAgentTool[]; diff --git a/src/cli/capability-cli.test.ts b/src/cli/capability-cli.test.ts index 7ff8c40a68e..f762ffaeec5 100644 --- a/src/cli/capability-cli.test.ts +++ b/src/cli/capability-cli.test.ts @@ -712,7 +712,7 @@ describe("capability cli", () => { const preparedParams = firstPreparedModelParams(); expect(preparedParams?.agentId).toBe("main"); expect(preparedParams?.allowMissingApiKeyModes).toEqual(["aws-sdk"]); - expect(preparedParams?.skipPiDiscovery).toBe(true); + expect(preparedParams?.skipAgentDiscovery).toBe(true); const call = firstCompletionCall(); expect(call?.context?.messages?.[0]?.role).toBe("user"); expect(call?.context?.messages?.[0]?.content).toBe("hello"); @@ -725,7 +725,7 @@ describe("capability cli", () => { const params = firstPreparedModelParams(); expect(params?.modelRef).toBe("mistral/mistral-medium-3-5"); expect(params?.allowBundledStaticCatalogFallback).toBe(true); - expect(params?.skipPiDiscovery).toBe(true); + expect(params?.skipAgentDiscovery).toBe(true); }); it("does not enable bundled static catalog fallback without an explicit provider/model override", async () => { diff --git a/src/cli/capability-cli.ts b/src/cli/capability-cli.ts index 7bc27c4c29a..793b9cc133b 100644 --- a/src/cli/capability-cli.ts +++ b/src/cli/capability-cli.ts @@ -728,14 +728,14 @@ async function runModelRun(params: { modelRef, allowMissingApiKeyModes: ["aws-sdk"], ...(hasExplicitProviderModelOverride ? { allowBundledStaticCatalogFallback: true } : {}), - skipPiDiscovery: true, + skipAgentDiscovery: true, }); if ("error" in prepared) { throw new Error(prepared.error); } if (prepared.selection.provider === "codex") { throw new Error( - 'The codex provider is served by the Codex app-server agent runtime, not the local simple-completion transport. Use an openai/ ref with agents.defaults.agentRuntime.id: "codex", run through the gateway, or use /codex commands.', + 'The codex provider is served by the Codex app-server agent runtime, not the local simple-completion transport. Use an openai/ ref with provider/model agentRuntime.id: "codex", run through the gateway, or use /codex commands.', ); } const localModelRunSystemPrompt = diff --git a/src/cli/gateway-cli/lifecycle.runtime.ts b/src/cli/gateway-cli/lifecycle.runtime.ts index 6e04d3fa36a..141b9946ed6 100644 --- a/src/cli/gateway-cli/lifecycle.runtime.ts +++ b/src/cli/gateway-cli/lifecycle.runtime.ts @@ -1,10 +1,10 @@ export { - abortEmbeddedPiRun, + abortEmbeddedAgentRun, getActiveEmbeddedRunCount, listActiveEmbeddedRunSessionIds, listActiveEmbeddedRunSessionKeys, waitForActiveEmbeddedRuns, -} from "../../agents/pi-embedded-runner/runs.js"; +} from "../../agents/embedded-agent-runner/runs.js"; export { markRestartAbortedMainSessions } from "../../agents/main-session-restart-recovery.js"; export { getRuntimeConfig } from "../../config/config.js"; export { diff --git a/src/cli/gateway-cli/run-loop.test.ts b/src/cli/gateway-cli/run-loop.test.ts index 7c3adba6332..7e2d5b99c77 100644 --- a/src/cli/gateway-cli/run-loop.test.ts +++ b/src/cli/gateway-cli/run-loop.test.ts @@ -73,7 +73,7 @@ const respawnGatewayProcessForUpdate = vi.fn< const markUpdateRestartSentinelFailure = vi.fn<(reason: string) => Promise>( async (_reason: string) => null, ); -const abortEmbeddedPiRun = vi.fn( +const abortEmbeddedAgentRun = vi.fn( (_sessionId?: string, _opts?: { mode?: "all" | "compacting" }) => false, ); const getActiveEmbeddedRunCount = vi.fn(() => 0); @@ -161,9 +161,9 @@ vi.mock("../../tasks/task-registry.maintenance.js", () => ({ getInspectableActiveTaskRestartBlockers: () => getInspectableActiveTaskRestartBlockers(), })); -vi.mock("../../agents/pi-embedded-runner/runs.js", () => ({ - abortEmbeddedPiRun: (sessionId?: string, opts?: { mode?: "all" | "compacting" }) => - abortEmbeddedPiRun(sessionId, opts), +vi.mock("../../agents/embedded-agent-runner/runs.js", () => ({ + abortEmbeddedAgentRun: (sessionId?: string, opts?: { mode?: "all" | "compacting" }) => + abortEmbeddedAgentRun(sessionId, opts), getActiveEmbeddedRunCount: () => getActiveEmbeddedRunCount(), listActiveEmbeddedRunSessionIds: () => listActiveEmbeddedRunSessionIds(), listActiveEmbeddedRunSessionKeys: () => listActiveEmbeddedRunSessionKeys(), @@ -498,8 +498,8 @@ describe("runGatewayLoop", () => { await new Promise((resolve) => setImmediate(resolve)); expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(undefined); - expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "compacting" }); - expect(abortEmbeddedPiRun).not.toHaveBeenCalledWith(undefined, { mode: "all" }); + expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { mode: "compacting" }); + expect(abortEmbeddedAgentRun).not.toHaveBeenCalledWith(undefined, { mode: "all" }); expectRestartCloseCall(close, 15_000); expect(start).toHaveBeenCalledTimes(2); @@ -530,8 +530,8 @@ describe("runGatewayLoop", () => { await new Promise((resolve) => setImmediate(resolve)); expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(undefined); - expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "compacting" }); - expect(abortEmbeddedPiRun).not.toHaveBeenCalledWith(undefined, { mode: "all" }); + expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { mode: "compacting" }); + expect(abortEmbeddedAgentRun).not.toHaveBeenCalledWith(undefined, { mode: "all" }); expectRestartCloseCall(close, 15_000); expect(start).toHaveBeenCalledTimes(2); @@ -562,8 +562,8 @@ describe("runGatewayLoop", () => { expect(waitForActiveTasks).toHaveBeenCalledWith(90_000); expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(90_000); - expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "compacting" }); - expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "all" }); + expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { mode: "compacting" }); + expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { mode: "all" }); expect(gatewayLog.warn).toHaveBeenCalledWith(ACTIVE_RUN_DRAIN_TIMEOUT_LOG); expect(gatewayLog.warn).toHaveBeenCalledWith(DRAIN_TIMEOUT_LOG); expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({ @@ -611,8 +611,8 @@ describe("runGatewayLoop", () => { expect(waitForActiveTasks).not.toHaveBeenCalled(); expect(waitForActiveEmbeddedRuns).not.toHaveBeenCalled(); - expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "compacting" }); - expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "all" }); + expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { mode: "compacting" }); + expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { mode: "all" }); expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({ cfg: { gateway: { @@ -666,7 +666,7 @@ describe("runGatewayLoop", () => { expect(waitForActiveTasks).not.toHaveBeenCalled(); expect(waitForActiveEmbeddedRuns).not.toHaveBeenCalled(); - expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "all" }); + expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { mode: "all" }); expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({ cfg: { gateway: { @@ -771,10 +771,10 @@ describe("runGatewayLoop", () => { expect(start).toHaveBeenCalledTimes(2); await new Promise((resolve) => setImmediate(resolve)); - expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "compacting" }); + expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { mode: "compacting" }); expect(waitForActiveTasks).toHaveBeenCalledWith(1_234); expect(waitForActiveEmbeddedRuns).toHaveBeenCalledWith(1_234); - expect(abortEmbeddedPiRun).toHaveBeenCalledWith(undefined, { mode: "all" }); + expect(abortEmbeddedAgentRun).toHaveBeenCalledWith(undefined, { mode: "all" }); expect(markRestartAbortedMainSessions).toHaveBeenCalledWith({ cfg: { gateway: { diff --git a/src/cli/gateway-cli/run-loop.ts b/src/cli/gateway-cli/run-loop.ts index 051ba0f09de..67b8d1b2793 100644 --- a/src/cli/gateway-cli/run-loop.ts +++ b/src/cli/gateway-cli/run-loop.ts @@ -459,7 +459,7 @@ export async function runGatewayLoop(params: { "restart.drain", async () => { const { - abortEmbeddedPiRun, + abortEmbeddedAgentRun, getRuntimeConfig, getInspectableActiveTaskRestartBlockers, getActiveEmbeddedRunCount, @@ -545,7 +545,7 @@ export async function runGatewayLoop(params: { // Best-effort abort for compacting runs so long compaction operations // don't hold session write locks across restart boundaries. if (activeRuns > 0) { - abortEmbeddedPiRun(undefined, { mode: "compacting" }); + abortEmbeddedAgentRun(undefined, { mode: "compacting" }); } if (activeTasks > 0 || activeRuns > 0) { @@ -563,7 +563,7 @@ export async function runGatewayLoop(params: { await markActiveMainSessionsForRestart( restartIntent.reason ?? "forced gateway restart", ); - abortEmbeddedPiRun(undefined, { mode: "all" }); + abortEmbeddedAgentRun(undefined, { mode: "all" }); } else { const stillPendingDrainLogger = createStillPendingDrainLogger(); let abortedAfterRunTimeout = false; @@ -582,7 +582,7 @@ export async function runGatewayLoop(params: { gatewayLog.warn( "active embedded run drain timeout reached; aborting active run(s) before restart", ); - abortEmbeddedPiRun(undefined, { mode: "all" }); + abortEmbeddedAgentRun(undefined, { mode: "all" }); abortedAfterRunTimeout = true; } tasksDrain = await tasksDrainPromise; @@ -598,7 +598,7 @@ export async function runGatewayLoop(params: { // Final best-effort abort to avoid carrying active runs into the // next lifecycle when drain time budget is exhausted. if (!abortedAfterRunTimeout) { - abortEmbeddedPiRun(undefined, { mode: "all" }); + abortEmbeddedAgentRun(undefined, { mode: "all" }); } } } diff --git a/src/cli/logs-cli.ts b/src/cli/logs-cli.ts index 1976e8e107a..ae16a299c69 100644 --- a/src/cli/logs-cli.ts +++ b/src/cli/logs-cli.ts @@ -13,7 +13,7 @@ import { formatErrorMessage } from "../infra/errors.js"; import { readConfiguredLogTail } from "../logging/log-tail.js"; import { parseLogLine } from "../logging/parse-log-line.js"; import { redactSensitiveLines, resolveRedactOptions } from "../logging/redact.js"; -import { formatTimestamp } from "../logging/timestamps.js"; +import { formatTimestamp, isValidTimeZone } from "../logging/timestamps.js"; import { createLazyImportLoader } from "../shared/lazy-promise.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; import { formatDocsLink } from "../terminal/links.js"; @@ -23,16 +23,6 @@ import { colorize, isRich, theme } from "../terminal/theme.js"; import { formatCliCommand } from "./command-format.js"; import { addGatewayClientOptions, callGatewayFromCli } from "./gateway-rpc.js"; -type LogsCliRuntimeModule = typeof import("./logs-cli.runtime.js"); - -const logsCliRuntimeLoader = createLazyImportLoader( - () => import("./logs-cli.runtime.js"), -); - -async function loadLogsCliRuntime(): Promise { - return logsCliRuntimeLoader.load(); -} - type LogsTailPayload = { file?: string; source?: string; @@ -49,6 +39,8 @@ type LogsTailPayload = { localFallback?: boolean; }; +type LogsCliRuntimeModule = typeof import("./logs-cli.runtime.js"); + type LogCursorState = { gateway?: number; journal?: string; @@ -63,6 +55,10 @@ class JournalFallbackUnavailableError extends Error { } } +async function loadLogsCliRuntime(): Promise { + return await import("./logs-cli.runtime.js"); +} + type LogsCliOptions = { limit?: string; maxBytes?: string; @@ -313,10 +309,7 @@ function parseJournalctlOutput(output: string): { lines: string[]; cursor?: stri return { lines, cursor }; } -function resolveLogsSystemdUnitName( - runtime: LogsCliRuntimeModule, - env: NodeJS.ProcessEnv, -): string { +function resolveLogsSystemdUnitName(runtime: LogsCliRuntimeModule, env: NodeJS.ProcessEnv): string { const override = env.OPENCLAW_SYSTEMD_UNIT?.trim(); if (override) { return override.endsWith(".service") ? override : `${override}.service`; @@ -446,12 +439,11 @@ async function emitGatewayError( emitJsonLine: (payload: Record, toStdErr?: boolean) => boolean, errorLine: (text: string) => boolean, ) { - const runtime = await loadLogsCliRuntime(); const message = "Gateway not reachable. Is it running and accessible?"; const hint = `Hint: run \`${formatCliCommand("openclaw doctor")}\`.`; const errorText = formatErrorMessage(err); - const details = runtime.buildGatewayConnectionDetails({ url: opts.url }); + const details = buildGatewayConnectionDetails({ url: opts.url }); if (mode === "json") { if ( !emitJsonLine( diff --git a/src/cli/models-cli.ts b/src/cli/models-cli.ts index 80ab9fce89e..6c7ace1a22c 100644 --- a/src/cli/models-cli.ts +++ b/src/cli/models-cli.ts @@ -17,10 +17,7 @@ export function registerModelsCli(program: Command) { .description("Model discovery, scanning, and configuration") .option("--status-json", "Output JSON (alias for `models status --json`)", false) .option("--status-plain", "Plain output (alias for `models status --plain`)", false) - .option( - "--agent ", - "Agent id to inspect (overrides OPENCLAW_AGENT_DIR/PI_CODING_AGENT_DIR)", - ) + .option("--agent ", "Agent id to inspect (overrides OPENCLAW_AGENT_DIR)") .addHelpText( "after", () => @@ -66,10 +63,7 @@ export function registerModelsCli(program: Command) { .option("--probe-timeout ", "Per-probe timeout in ms") .option("--probe-concurrency ", "Concurrent probes") .option("--probe-max-tokens ", "Probe max tokens (best-effort)") - .option( - "--agent ", - "Agent id to inspect (overrides OPENCLAW_AGENT_DIR/PI_CODING_AGENT_DIR)", - ) + .option("--agent ", "Agent id to inspect (overrides OPENCLAW_AGENT_DIR)") .action(async (opts, command) => { await withModelsRuntime(async ({ defaultRuntime, resolveModelAgentOption }) => { const agent = resolveModelAgentOption(command, opts); @@ -333,10 +327,7 @@ export function registerModelsCli(program: Command) { .option("--provider ", "Provider id registered by a plugin") .option("--method ", "Provider auth method id") .option("--device-code", "Use the provider device-code auth method", false) - .option( - "--profile-id ", - "Auth profile id override for single-profile login methods", - ) + .option("--profile-id ", "Auth profile id override for single-profile login methods") .option("--set-default", "Apply the provider's default model recommendation", false) .action(async (opts, command) => { if (opts.deviceCode && typeof opts.method === "string" && opts.method !== "device-code") { diff --git a/src/cli/plugins-cli.list.test.ts b/src/cli/plugins-cli.list.test.ts index 05e44cf55ad..0c1d12bf720 100644 --- a/src/cli/plugins-cli.list.test.ts +++ b/src/cli/plugins-cli.list.test.ts @@ -229,7 +229,7 @@ describe("plugins cli list", () => { expect(output).toContain('Configured runtime "acpx" requires the ACPX Runtime plugin'); expect(output).toContain("Set plugins.entries.acpx.enabled=true"); expect(output).toContain("disable ACP/acpx in acp config"); - expect(output).not.toContain('runtime policy to "pi"'); + expect(output).not.toContain('runtime policy to "openclaw"'); expect(output).not.toContain("openclaw plugins install @openclaw/acpx"); expect(output).not.toContain("No plugin issues detected."); }); @@ -252,7 +252,7 @@ describe("plugins cli list", () => { expect(output).toContain('Configured runtime "acpx" requires the ACPX Runtime plugin'); expect(output).toContain('Enable the "acpx" plugin'); expect(output).toContain("disable ACP/acpx in acp config"); - expect(output).not.toContain('runtime policy to "pi"'); + expect(output).not.toContain('runtime policy to "openclaw"'); expect(output).not.toContain("openclaw plugins install @openclaw/acpx"); expect(output).not.toContain("No plugin issues detected."); }); diff --git a/src/cli/plugins-cli.runtime.ts b/src/cli/plugins-cli.runtime.ts index 91be8d4ed54..f3427ac35ef 100644 --- a/src/cli/plugins-cli.runtime.ts +++ b/src/cli/plugins-cli.runtime.ts @@ -95,7 +95,9 @@ function formatBlockedRuntimePluginGuidance(params: { }): string | undefined { const pluginId = params.pluginId; const alternative = - pluginId === "acpx" ? "disable ACP/acpx in acp config" : 'change the runtime policy to "pi"'; + pluginId === "acpx" + ? "disable ACP/acpx in acp config" + : 'change the runtime policy to "openclaw"'; if (params.cfg.plugins?.enabled === false) { return `Enable plugin loading and the "${pluginId}" plugin, or ${alternative}.`; } @@ -116,7 +118,7 @@ function formatDisabledRuntimePluginGuidance(params: { const alternative = params.pluginId === "acpx" ? "disable ACP/acpx in acp config" - : 'change the runtime policy to "pi"'; + : 'change the runtime policy to "openclaw"'; if (Array.isArray(allow) && allow.length > 0 && !allow.includes(params.pluginId)) { return `Add "${params.pluginId}" to plugins.allow and enable the plugin, or ${alternative}.`; } @@ -136,7 +138,6 @@ function collectConfiguredRuntimePluginWarnings(params: { return collectConfiguredRuntimePluginIds(params.cfg, params.env, { includeEnvRuntime: false, includeImplicitRuntimePreferences: false, - includeLegacyAgentRuntimes: false, }).flatMap((runtimeId) => { const candidate = resolveConfiguredRuntimePluginInstallCandidate(runtimeId); if (!candidate || enabledPluginIds.has(runtimeId)) { diff --git a/src/cli/skills-cli.test.ts b/src/cli/skills-cli.test.ts index 057e81206a5..e247a3ea3e5 100644 --- a/src/cli/skills-cli.test.ts +++ b/src/cli/skills-cli.test.ts @@ -4,7 +4,7 @@ import { createEmptyInstallChecks } from "./requirements-test-fixtures.js"; import { formatSkillInfo, formatSkillsCheck, formatSkillsList } from "./skills-cli.format.js"; // Unit tests: don't pay the runtime cost of loading/parsing the real skills loader. -vi.mock("@earendil-works/pi-coding-agent", () => ({ +vi.mock("openclaw/plugin-sdk/agent-sessions", () => ({ loadSkillsFromDir: () => ({ skills: [] }), formatSkillsForPrompt: () => "", })); diff --git a/src/commands/agent-command.test-mocks.ts b/src/commands/agent-command.test-mocks.ts index a5cf06d7f09..c14b3a85e3f 100644 --- a/src/commands/agent-command.test-mocks.ts +++ b/src/commands/agent-command.test-mocks.ts @@ -43,9 +43,9 @@ vi.mock("../acp/control-plane/manager.js", () => ({ getAcpSessionManager: vi.fn(() => acpManagerMock.current), })); -vi.mock("../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: vi.fn(), +vi.mock("../agents/embedded-agent.js", () => ({ + abortEmbeddedAgentRun: vi.fn().mockReturnValue(false), + runEmbeddedAgent: vi.fn(), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, })); diff --git a/src/commands/agent.acp.test.ts b/src/commands/agent.acp.test.ts index c68c93105b9..f9fd772777d 100644 --- a/src/commands/agent.acp.test.ts +++ b/src/commands/agent.acp.test.ts @@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import "./agent-command.test-mocks.js"; import * as acpManagerModule from "../acp/control-plane/manager.js"; import { AcpRuntimeError } from "../acp/runtime/errors.js"; -import * as embeddedModule from "../agents/pi-embedded.js"; +import * as embeddedModule from "../agents/embedded-agent.js"; import * as configIoModule from "../config/io.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { RuntimeEnv } from "../runtime.js"; @@ -115,7 +115,7 @@ vi.mock("../agents/command/attempt-execution.runtime.js", () => { }); const loadConfigSpy = vi.spyOn(configIoModule, "loadConfig"); -const runEmbeddedPiAgentSpy = vi.spyOn(embeddedModule, "runEmbeddedPiAgent"); +const runEmbeddedAgentSpy = vi.spyOn(embeddedModule, "runEmbeddedAgent"); const getAcpSessionManagerSpy = vi.spyOn(acpManagerModule, "getAcpSessionManager"); const runtime = createThrowingTestRuntime(); @@ -332,7 +332,7 @@ async function runAcpSessionWithPolicyOverridesAndExpectBlocked(params: { await expectAcpCommandRejects("agent:codex:acp:test", "ACP_DISPATCH_DISABLED"); expect(runTurn).not.toHaveBeenCalled(); - expect(runEmbeddedPiAgentSpy).not.toHaveBeenCalled(); + expect(runEmbeddedAgentSpy).not.toHaveBeenCalled(); }); } @@ -357,7 +357,7 @@ async function expectAcpCommandRejects( describe("agentCommand ACP runtime routing", () => { beforeEach(() => { vi.clearAllMocks(); - runEmbeddedPiAgentSpy.mockResolvedValue({ + runEmbeddedAgentSpy.mockResolvedValue({ payloads: [{ text: "embedded" }], meta: { durationMs: 5, @@ -375,7 +375,7 @@ describe("agentCommand ACP runtime routing", () => { expect(runTurnInput?.sessionKey).toBe("agent:codex:acp:test"); expect(runTurnInput?.text).toBe(" ping\n"); expect(runTurnInput?.mode).toBe("prompt"); - expect(runEmbeddedPiAgentSpy).not.toHaveBeenCalled(); + expect(runEmbeddedAgentSpy).not.toHaveBeenCalled(); const hasAckLog = vi .mocked(runtime.log) .mock.calls.some(([first]) => typeof first === "string" && first.includes("ACP_OK")); @@ -445,7 +445,7 @@ describe("agentCommand ACP runtime routing", () => { "ACP metadata is missing", ); expect(runTurn).not.toHaveBeenCalled(); - expect(runEmbeddedPiAgentSpy).not.toHaveBeenCalled(); + expect(runEmbeddedAgentSpy).not.toHaveBeenCalled(); }); }); @@ -478,7 +478,7 @@ describe("agentCommand ACP runtime routing", () => { "not allowed by policy", ); expect(runTurn).not.toHaveBeenCalled(); - expect(runEmbeddedPiAgentSpy).not.toHaveBeenCalled(); + expect(runEmbeddedAgentSpy).not.toHaveBeenCalled(); }); }); @@ -501,7 +501,7 @@ describe("agentCommand ACP runtime routing", () => { const runTurnInput = firstRunTurnInput(runTurn); expect(runTurnInput?.sessionKey).toBe("agent:kimi:acp:test"); expect(runTurnInput?.text).toBe("ping"); - expect(runEmbeddedPiAgentSpy).not.toHaveBeenCalled(); + expect(runEmbeddedAgentSpy).not.toHaveBeenCalled(); }); }); }); diff --git a/src/commands/agent.test.ts b/src/commands/agent.test.ts index 25697397763..0c00529c38a 100644 --- a/src/commands/agent.test.ts +++ b/src/commands/agent.test.ts @@ -6,9 +6,9 @@ import "./agent-command.test-mocks.js"; import { testing as acpManagerTesting } from "../acp/control-plane/manager.js"; import * as authProfileStoreModule from "../agents/auth-profiles/store.js"; import * as attemptExecutionRuntime from "../agents/command/attempt-execution.runtime.js"; +import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import { loadManifestModelCatalog, loadModelCatalog } from "../agents/model-catalog.js"; import * as modelSelectionModule from "../agents/model-selection.js"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { BASE_THINKING_LEVELS } from "../auto-reply/thinking.shared.js"; import * as runtimeSnapshotModule from "../config/runtime-snapshot.js"; import { loadSessionStore } from "../config/sessions/store-load.js"; @@ -95,7 +95,7 @@ vi.mock("../agents/command/attempt-execution.runtime.js", () => { const authProfileId = providerOverride === authProfileProvider ? sessionEntry?.authProfileOverride : undefined; - return await runEmbeddedPiAgent({ + return await runEmbeddedAgent({ sessionId: params.sessionId, sessionKey: params.sessionKey, agentId: params.sessionAgentId, @@ -303,7 +303,7 @@ function createDefaultAgentResult(params?: { } function getLastEmbeddedCall() { - const calls = vi.mocked(runEmbeddedPiAgent).mock.calls; + const calls = vi.mocked(runEmbeddedAgent).mock.calls; return calls[calls.length - 1]?.[0]; } @@ -355,7 +355,7 @@ beforeEach(() => { resetAgentRunContextForTest(); acpManagerTesting.resetAcpSessionManagerForTests(); runtimeSnapshotModule.clearRuntimeConfigSnapshot(); - vi.mocked(runEmbeddedPiAgent).mockResolvedValue(createDefaultAgentResult()); + vi.mocked(runEmbeddedAgent).mockResolvedValue(createDefaultAgentResult()); vi.mocked(loadManifestModelCatalog).mockReturnValue([]); vi.mocked(loadModelCatalog).mockResolvedValue([]); vi.mocked(modelSelectionModule.isCliProvider).mockImplementation(() => false); @@ -393,7 +393,7 @@ describe("agentCommand", () => { }); }); - it("does not enable Codex for one-shot OpenAI overrides when the provider forces PI", async () => { + it("does not enable Codex for one-shot OpenAI overrides when the provider forces OpenClaw", async () => { await withTempHome(async (home) => { const storePath = path.join(home, "sessions.json"); const cfg = mockConfig(home, storePath, { models: undefined }); @@ -401,7 +401,7 @@ describe("agentCommand", () => { providers: { openai: { baseUrl: "https://api.openai.com/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, @@ -485,7 +485,7 @@ describe("agentCommand", () => { await withTempHome(async (home) => { const store = path.join(home, "sessions.json"); mockConfig(home, store); - vi.mocked(runEmbeddedPiAgent).mockResolvedValue( + vi.mocked(runEmbeddedAgent).mockResolvedValue( createDefaultAgentResult({ payloads: [{ text: "json-reply", mediaUrl: "http://x.test/a.jpg" }], durationMs: 42, @@ -535,7 +535,7 @@ describe("agentCommand", () => { const store = path.join(home, "sessions.json"); mockConfig(home, store); const base = createDefaultAgentResult({ payloads: [{ text: "assistant-visible" }] }); - vi.mocked(runEmbeddedPiAgent).mockResolvedValueOnce({ + vi.mocked(runEmbeddedAgent).mockResolvedValueOnce({ ...base, meta: { ...base.meta, @@ -560,7 +560,7 @@ describe("agentCommand", () => { mockConfig(home, store); const sendMessageTelegram = vi.fn(async () => undefined); const base = createDefaultAgentResult({ payloads: [{ text: "assistant-visible" }] }); - vi.mocked(runEmbeddedPiAgent).mockResolvedValueOnce({ + vi.mocked(runEmbeddedAgent).mockResolvedValueOnce({ ...base, meta: { ...base.meta, @@ -785,7 +785,7 @@ describe("agentCommand", () => { }); }); - vi.mocked(runEmbeddedPiAgent).mockImplementationOnce(async (params) => { + vi.mocked(runEmbeddedAgent).mockImplementationOnce(async (params) => { const runId = (params as { runId?: string } | undefined)?.runId ?? "run"; const data = { text: "hello", delta: "hello" }; ( @@ -824,7 +824,7 @@ describe("agentCommand", () => { }); }); - vi.mocked(runEmbeddedPiAgent).mockImplementationOnce(async (params) => { + vi.mocked(runEmbeddedAgent).mockImplementationOnce(async (params) => { ( params as { onAgentEvent?: (evt: { stream: string; data: Record }) => void; @@ -876,7 +876,7 @@ describe("agentCommand", () => { { id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" }, { id: "gpt-5.4", name: "GPT-5.2", provider: "openai" }, ]); - vi.mocked(runEmbeddedPiAgent) + vi.mocked(runEmbeddedAgent) .mockRejectedValueOnce(Object.assign(new Error("rate limited"), { status: 429 })) .mockResolvedValueOnce({ payloads: [{ text: "ok" }], @@ -895,7 +895,7 @@ describe("agentCommand", () => { ); const attempts = vi - .mocked(runEmbeddedPiAgent) + .mocked(runEmbeddedAgent) .mock.calls.map((call) => ({ provider: call[0]?.provider, model: call[0]?.model })); expect(attempts).toEqual([ { provider: "anthropic", model: "claude-opus-4-6" }, @@ -934,7 +934,7 @@ describe("agentCommand", () => { { id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" }, { id: "gpt-5.4", name: "GPT-5.4", provider: "openai" }, ]); - vi.mocked(runEmbeddedPiAgent).mockRejectedValueOnce(new Error("connect ECONNREFUSED")); + vi.mocked(runEmbeddedAgent).mockRejectedValueOnce(new Error("connect ECONNREFUSED")); await expect( agentCommand( @@ -947,7 +947,7 @@ describe("agentCommand", () => { ).rejects.toThrow("connect ECONNREFUSED"); const attempts = vi - .mocked(runEmbeddedPiAgent) + .mocked(runEmbeddedAgent) .mock.calls.map((call) => ({ provider: call[0]?.provider, model: call[0]?.model })); expect(attempts).toEqual([{ provider: "ollama", model: "qwen3.5:27b" }]); }); diff --git a/src/commands/auth-choice.test.ts b/src/commands/auth-choice.test.ts index 05180784cc0..eaf2f552878 100644 --- a/src/commands/auth-choice.test.ts +++ b/src/commands/auth-choice.test.ts @@ -568,7 +568,6 @@ describe("applyAuthChoice", () => { const lifecycle = createAuthTestLifecycle([ "OPENCLAW_STATE_DIR", "OPENCLAW_AGENT_DIR", - "PI_CODING_AGENT_DIR", "ANTHROPIC_API_KEY", "OPENROUTER_API_KEY", "HF_TOKEN", @@ -588,7 +587,6 @@ describe("applyAuthChoice", () => { const agentDir = path.join(stateDir, "agent"); process.env.OPENCLAW_STATE_DIR = stateDir; process.env.OPENCLAW_AGENT_DIR = agentDir; - process.env.PI_CODING_AGENT_DIR = agentDir; } function createPrompter(overrides: Partial): WizardPrompter { return createWizardPrompter(overrides, { defaultSelect: "" }); diff --git a/src/commands/chutes-oauth.ts b/src/commands/chutes-oauth.ts index 86da7aafa14..26f04d12456 100644 --- a/src/commands/chutes-oauth.ts +++ b/src/commands/chutes-oauth.ts @@ -1,6 +1,6 @@ import { randomBytes } from "node:crypto"; import { createServer } from "node:http"; -import type { OAuthCredentials } from "@earendil-works/pi-ai"; +import type { OAuthCredentials } from "openclaw/plugin-sdk/llm"; import type { ChutesOAuthAppConfig } from "../agents/chutes-oauth.js"; import { CHUTES_AUTHORIZE_ENDPOINT, diff --git a/src/commands/doctor-bootstrap-size.test.ts b/src/commands/doctor-bootstrap-size.test.ts index cdc9b5d7ac5..197babc6323 100644 --- a/src/commands/doctor-bootstrap-size.test.ts +++ b/src/commands/doctor-bootstrap-size.test.ts @@ -21,7 +21,7 @@ vi.mock("../agents/bootstrap-files.js", () => ({ resolveBootstrapContextForRun, })); -vi.mock("../agents/pi-embedded-helpers.js", () => ({ +vi.mock("../agents/embedded-agent-helpers.js", () => ({ resolveBootstrapMaxChars, resolveBootstrapTotalMaxChars, })); diff --git a/src/commands/doctor-bootstrap-size.ts b/src/commands/doctor-bootstrap-size.ts index d8361e18b2f..6391707ef86 100644 --- a/src/commands/doctor-bootstrap-size.ts +++ b/src/commands/doctor-bootstrap-size.ts @@ -7,7 +7,7 @@ import { resolveBootstrapContextForRun } from "../agents/bootstrap-files.js"; import { resolveBootstrapMaxChars, resolveBootstrapTotalMaxChars, -} from "../agents/pi-embedded-helpers.js"; +} from "../agents/embedded-agent-helpers.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { note } from "../terminal/note.js"; diff --git a/src/commands/doctor-config-flow.test.ts b/src/commands/doctor-config-flow.test.ts index 960d36306ac..3acbea646f4 100644 --- a/src/commands/doctor-config-flow.test.ts +++ b/src/commands/doctor-config-flow.test.ts @@ -1460,7 +1460,7 @@ describe("doctor config flow", () => { const result = await runDoctorConfigWithInput({ config: { gateway: { auth: { mode: "token", token: 123 } }, - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, }, run: loadAndMaybeMigrateDoctorConfig, }); @@ -1731,7 +1731,7 @@ describe("doctor config flow", () => { config: { bridge: { bind: "auto" }, gateway: { auth: { mode: "token", token: "ok", extra: true } }, - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, session: { maintenance: { rotateBytes: "10mb", diff --git a/src/commands/doctor-cron.test.ts b/src/commands/doctor-cron.test.ts index 325e1bc0faf..978fd271818 100644 --- a/src/commands/doctor-cron.test.ts +++ b/src/commands/doctor-cron.test.ts @@ -238,7 +238,7 @@ describe("maybeRepairLegacyCronStore", () => { await writeCronStore(storePath, [ { id: "alias-pinned", - name: "Alias pinned", + name: "Alias the native runtime", enabled: true, createdAtMs: Date.parse("2026-05-01T00:00:00.000Z"), updatedAtMs: Date.parse("2026-05-01T00:00:00.000Z"), @@ -259,7 +259,7 @@ describe("maybeRepairLegacyCronStore", () => { cron: { store: storePath }, agents: { defaults: { - model: { primary: "pi:opus", fallbacks: [] }, + model: { primary: "test:opus", fallbacks: [] }, }, }, }, diff --git a/src/commands/doctor-legacy-config.migrations.test.ts b/src/commands/doctor-legacy-config.migrations.test.ts index 56147e52aaa..befa64f7e3a 100644 --- a/src/commands/doctor-legacy-config.migrations.test.ts +++ b/src/commands/doctor-legacy-config.migrations.test.ts @@ -680,7 +680,7 @@ describe("normalizeCompatibilityConfigValues", () => { agentRuntime: { id: "claude-cli" }, model: "anthropic/claude-opus-4-7", models: { - "anthropic/claude-opus-4-7": { agentRuntime: { id: "pi" } }, + "anthropic/claude-opus-4-7": { agentRuntime: { id: "openclaw" } }, }, }, ], @@ -689,7 +689,7 @@ describe("normalizeCompatibilityConfigValues", () => { expect(res.config.agents?.list?.[0]?.agentRuntime).toEqual({ id: "claude-cli" }); expect(res.config.agents?.list?.[0]?.models).toEqual({ - "anthropic/claude-opus-4-7": { agentRuntime: { id: "pi" } }, + "anthropic/claude-opus-4-7": { agentRuntime: { id: "openclaw" } }, }); expect(res.changes).toStrictEqual([]); }); diff --git a/src/commands/doctor-session-state-providers.test.ts b/src/commands/doctor-session-state-providers.test.ts index 7d0ef3ce6e3..50ce067edab 100644 --- a/src/commands/doctor-session-state-providers.test.ts +++ b/src/commands/doctor-session-state-providers.test.ts @@ -107,7 +107,7 @@ describe("doctor session state provider routes", () => { agents: { defaults: { model: { primary: "openai/gpt-5.5" }, - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, }, }, }, @@ -153,7 +153,7 @@ describe("doctor session state provider routes", () => { [sessionKey]: { defaultProvider: "github-copilot", configuredModelRefs: ["github-copilot/gpt-5-mini"], - runtime: "pi", + runtime: "openclaw", }, }, }); @@ -222,7 +222,7 @@ describe("doctor session state provider routes", () => { [sessionKey]: { defaultProvider: "github-copilot", configuredModelRefs: ["github-copilot/gpt-5-mini"], - runtime: "pi", + runtime: "openclaw", }, }, }); @@ -260,7 +260,7 @@ describe("doctor session state provider routes", () => { [sessionKey]: { defaultProvider: "github-copilot", configuredModelRefs: ["github-copilot/gpt-5-mini", "openai-codex/gpt-5.4"], - runtime: "pi", + runtime: "openclaw", }, }, }); @@ -340,7 +340,7 @@ describe("doctor session state provider routes", () => { [sessionKey]: { defaultProvider: "openai", configuredModelRefs: ["openai/gpt-5.5"], - runtime: "pi", + runtime: "openclaw", }, }, }); @@ -419,7 +419,7 @@ describe("doctor session state provider routes", () => { [sessionKey]: { defaultProvider: "anthropic", configuredModelRefs: ["anthropic/claude-opus-4.7"], - runtime: "pi", + runtime: "openclaw", }, }, }); @@ -457,7 +457,7 @@ describe("doctor session state provider routes", () => { [sessionKey]: { defaultProvider: "openai", configuredModelRefs: ["openai/gpt-5.5"], - runtime: "pi", + runtime: "openclaw", }, }, }); diff --git a/src/commands/doctor-state-integrity.test.ts b/src/commands/doctor-state-integrity.test.ts index 8919e563bfb..61b783da73d 100644 --- a/src/commands/doctor-state-integrity.test.ts +++ b/src/commands/doctor-state-integrity.test.ts @@ -34,7 +34,6 @@ type EnvSnapshot = { OPENCLAW_STATE_DIR?: string; OPENCLAW_OAUTH_DIR?: string; OPENCLAW_AGENT_DIR?: string; - PI_CODING_AGENT_DIR?: string; }; function captureEnv(): EnvSnapshot { @@ -44,7 +43,6 @@ function captureEnv(): EnvSnapshot { OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR, OPENCLAW_OAUTH_DIR: process.env.OPENCLAW_OAUTH_DIR, OPENCLAW_AGENT_DIR: process.env.OPENCLAW_AGENT_DIR, - PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR, }; } @@ -161,7 +159,6 @@ describe("doctor state integrity oauth dir checks", () => { process.env.OPENCLAW_STATE_DIR = path.join(tempHome, ".openclaw"); delete process.env.OPENCLAW_OAUTH_DIR; delete process.env.OPENCLAW_AGENT_DIR; - delete process.env.PI_CODING_AGENT_DIR; fs.mkdirSync(process.env.OPENCLAW_STATE_DIR, { recursive: true, mode: 0o700 }); noteMock.mockClear(); }); @@ -277,7 +274,6 @@ describe("doctor state integrity oauth dir checks", () => { "agent", ); process.env.OPENCLAW_AGENT_DIR = legacyAgentDir; - process.env.PI_CODING_AGENT_DIR = legacyAgentDir; const text = await runStateIntegrityText({ agents: { diff --git a/src/commands/doctor/shared/codex-route-warnings.test.ts b/src/commands/doctor/shared/codex-route-warnings.test.ts index b9b84233576..dedf7536ce4 100644 --- a/src/commands/doctor/shared/codex-route-warnings.test.ts +++ b/src/commands/doctor/shared/codex-route-warnings.test.ts @@ -184,7 +184,7 @@ describe("collectCodexRouteWarnings", () => { [ "- Codex runtime is selected, but the Codex plugin is disabled.", "- agents.defaults.model.primary: gpt-5.5 resolves to openai/gpt-5.5 with Codex runtime while the Codex plugin is disabled by config.", - "- Run `openclaw doctor --fix`: it enables plugins.entries.codex, or set the affected OpenAI models to a PI runtime policy.", + "- Run `openclaw doctor --fix`: it enables plugins.entries.codex, or set the affected OpenAI models to an OpenClaw runtime policy.", ].join("\n"), ]); }); @@ -786,7 +786,7 @@ describe("collectCodexRouteWarnings", () => { providers: { openai: { baseUrl: "https://proxy.example.test/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, }, }, }, @@ -1360,7 +1360,7 @@ describe("collectCodexRouteWarnings", () => { id: "worker", model: "anthropic/claude-sonnet-4-6", models: { - "anthropic/claude-sonnet-4-6": { agentRuntime: { id: "pi" } }, + "anthropic/claude-sonnet-4-6": { agentRuntime: { id: "openclaw" } }, }, }, ], @@ -1588,7 +1588,7 @@ describe("collectCodexRouteWarnings", () => { agentRuntime: { id: "codex" }, }, { - id: "pi-worker", + id: "native-worker", model: "anthropic/claude-sonnet-4-6", }, ], @@ -1793,7 +1793,7 @@ describe("collectCodexRouteWarnings", () => { providers: { openai: { baseUrl: "https://api.openai.com/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, @@ -1834,7 +1834,7 @@ describe("collectCodexRouteWarnings", () => { modelId: "gpt-5.4", config: result.cfg, }).runtime, - ).toBe("pi"); + ).toBe("openclaw"); }); it("repairs configured Codex model refs to canonical OpenAI refs with model-scoped Codex runtime", () => { @@ -2633,7 +2633,7 @@ describe("collectCodexRouteWarnings", () => { model: "gpt-5.5", models: { "openai/gpt-5.5": { - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, }, }, }, @@ -2756,7 +2756,7 @@ describe("collectCodexRouteWarnings", () => { expect(result.cfg.plugins?.allow).toEqual(["openai", "codex"]); }); - it("keeps the Codex plugin disabled when OpenAI routes explicitly use the PI runtime", () => { + it("keeps the Codex plugin disabled when OpenAI routes explicitly use the OpenClaw runtime", () => { const result = maybeRepairCodexRoutes({ cfg: { plugins: { @@ -2767,7 +2767,7 @@ describe("collectCodexRouteWarnings", () => { models: { providers: { openai: { - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, @@ -2790,10 +2790,10 @@ describe("collectCodexRouteWarnings", () => { modelId: "gpt-5.5", config: result.cfg, }).runtime, - ).toBe("pi"); + ).toBe("openclaw"); }); - it("keeps the Codex plugin disabled when an auth-profiled OpenAI route explicitly uses the PI runtime", () => { + it("keeps the Codex plugin disabled when an auth-profiled OpenAI route explicitly uses the OpenClaw runtime", () => { const result = maybeRepairCodexRoutes({ cfg: { plugins: { @@ -2806,7 +2806,7 @@ describe("collectCodexRouteWarnings", () => { model: "openai/gpt-5.5@work", models: { "openai/gpt-5.5": { - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, }, }, }, @@ -2909,14 +2909,14 @@ describe("collectCodexRouteWarnings", () => { expect(result.cfg.plugins?.entries?.codex?.enabled).toBe(true); }); - it("keeps repaired OpenAI refs on Codex runtime even when the OpenAI provider is otherwise PI/API-key routed", () => { + it("keeps repaired OpenAI refs on Codex runtime even when the OpenAI provider is otherwise OpenClaw/API-key routed", () => { const result = maybeRepairCodexRoutes({ cfg: { models: { providers: { openai: { baseUrl: "https://api.openai.com/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, @@ -2950,7 +2950,7 @@ describe("collectCodexRouteWarnings", () => { providers: { openai: { baseUrl: "https://api.openai.com/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, @@ -2980,7 +2980,7 @@ describe("collectCodexRouteWarnings", () => { }); expect(result.cfg.agents?.list?.[1]?.model).toBe("openai/gpt-5.5"); expect(result.cfg.agents?.list?.[1]?.models?.["openai/gpt-5.5"]?.agentRuntime).toEqual({ - id: "pi", + id: "openclaw", }); expect( resolveAgentHarnessPolicy({ @@ -2996,7 +2996,7 @@ describe("collectCodexRouteWarnings", () => { agentId: "worker", config: result.cfg, }).runtime, - ).toBe("pi"); + ).toBe("openclaw"); }); it("preserves explicit model-scoped runtime pins when repairing legacy model map keys", () => { @@ -3007,7 +3007,7 @@ describe("collectCodexRouteWarnings", () => { models: { "openai-codex/gpt-5.5": { alias: "legacy-codex", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, }, }, }, @@ -3019,7 +3019,7 @@ describe("collectCodexRouteWarnings", () => { expect(result.cfg.agents?.defaults?.models).toEqual({ "openai/gpt-5.5": { alias: "legacy-codex", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, }, }); expect(result.changes.join("\n")).not.toContain( @@ -3031,7 +3031,7 @@ describe("collectCodexRouteWarnings", () => { modelId: "gpt-5.5", config: result.cfg, }).runtime, - ).toBe("pi"); + ).toBe("openclaw"); }); it("overwrites non-concrete model-scoped runtime pins when preserving Codex route intent", () => { @@ -3077,7 +3077,7 @@ describe("collectCodexRouteWarnings", () => { providers: { openai: { baseUrl: "https://api.openai.com/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, @@ -3105,7 +3105,7 @@ describe("collectCodexRouteWarnings", () => { modelId: "gpt-5.4", config: result.cfg, }).runtime, - ).toBe("pi"); + ).toBe("openclaw"); expect(result.changes).toStrictEqual([]); expect(result.warnings).toStrictEqual([ [ @@ -3158,7 +3158,7 @@ describe("collectCodexRouteWarnings", () => { providers: { openai: { baseUrl: "https://api.openai.com/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, @@ -3186,7 +3186,7 @@ describe("collectCodexRouteWarnings", () => { modelId: "gpt-5.4", config: result.cfg, }).runtime, - ).toBe("pi"); + ).toBe("openclaw"); expect(result.changes).toStrictEqual([]); expect(result.warnings).toStrictEqual([ [ @@ -3274,7 +3274,7 @@ describe("collectCodexRouteWarnings", () => { expect(store.main.agentRuntimeOverride).toBeUndefined(); }); - it("preserves canonical OpenAI sessions that are explicitly pinned to PI", () => { + it("preserves canonical OpenAI sessions that are explicitly pinned to OpenClaw", () => { const store: Record = { main: { sessionId: "s1", @@ -3283,8 +3283,8 @@ describe("collectCodexRouteWarnings", () => { model: "gpt-5.5", providerOverride: "openai", modelOverride: "gpt-5.4", - agentHarnessId: "pi", - agentRuntimeOverride: "pi", + agentHarnessId: "openclaw", + agentRuntimeOverride: "openclaw", authProfileOverride: "openai:work", }, }; @@ -3296,8 +3296,8 @@ describe("collectCodexRouteWarnings", () => { expect(result).toEqual({ changed: false, sessionKeys: [] }); expect(store.main.updatedAt).toBe(1); - expect(store.main.agentHarnessId).toBe("pi"); - expect(store.main.agentRuntimeOverride).toBe("pi"); + expect(store.main.agentHarnessId).toBe("openclaw"); + expect(store.main.agentRuntimeOverride).toBe("openclaw"); expect(store.main.authProfileOverride).toBe("openai:work"); }); diff --git a/src/commands/doctor/shared/codex-route-warnings.ts b/src/commands/doctor/shared/codex-route-warnings.ts index c469336405b..3f53fa71f65 100644 --- a/src/commands/doctor/shared/codex-route-warnings.ts +++ b/src/commands/doctor/shared/codex-route-warnings.ts @@ -1,11 +1,11 @@ import fs from "node:fs"; +import { normalizeOptionalAgentRuntimeId } from "../../../agents/agent-runtime-id.js"; import { resolveConfiguredProviderFallback } from "../../../agents/configured-provider-fallback.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../../agents/defaults.js"; import { splitTrailingAuthProfile } from "../../../agents/model-ref-profile.js"; import { normalizeConfiguredProviderCatalogModelId } from "../../../agents/model-ref-shared.js"; import { resolveModelRuntimePolicy } from "../../../agents/model-runtime-policy.js"; import { openAIProviderUsesCodexRuntimeByDefault } from "../../../agents/openai-codex-routing.js"; -import { normalizeEmbeddedAgentRuntime } from "../../../agents/pi-embedded-runner/runtime.js"; import { normalizeProviderId } from "../../../agents/provider-id.js"; import { AGENT_MODEL_CONFIG_KEYS } from "../../../config/model-refs.js"; import { loadSessionStore, updateSessionStore } from "../../../config/sessions/store.js"; @@ -68,8 +68,7 @@ type CodexSessionRouteRepairSummary = { }; function normalizeRuntimeString(value: unknown): string | undefined { - const normalized = normalizeString(value); - return normalized ? normalizeEmbeddedAgentRuntime(normalized) : undefined; + return normalizeOptionalAgentRuntimeId(value); } function asAgentRuntimePolicyConfig(value: unknown): AgentRuntimePolicyConfig | undefined { @@ -77,6 +76,10 @@ function asAgentRuntimePolicyConfig(value: unknown): AgentRuntimePolicyConfig | return record ? { id: typeof record.id === "string" ? record.id : undefined } : undefined; } +function readLegacyDefaultsRuntime(defaults: unknown): AgentRuntimePolicyConfig | undefined { + return asAgentRuntimePolicyConfig(asMutableRecord(defaults)?.agentRuntime); +} + function isOpenAICodexModelRef(model: string | undefined): model is string { return normalizeString(model)?.startsWith("openai-codex/") === true; } @@ -582,7 +585,9 @@ function collectLegacyLosslessCompactionConfigs(params: { ignoreLegacyAgentRuntimePins?: boolean; }): LegacyLosslessCompactionConfig[] { const defaults = params.cfg.agents?.defaults; - const defaultsRuntime = params.ignoreLegacyAgentRuntimePins ? undefined : defaults?.agentRuntime; + const defaultsRuntime = params.ignoreLegacyAgentRuntimePins + ? undefined + : readLegacyDefaultsRuntime(defaults); const defaultModelRef = readAgentPrimaryModelRef(defaults); const defaultCompaction = asMutableRecord(defaults?.compaction); const hits = collectLegacyLosslessCompactionForAgent({ @@ -643,7 +648,9 @@ function collectUnsupportedCodexCompactionOverrides(params: { ignoreLegacyAgentRuntimePins?: boolean; }): UnsupportedCodexCompactionOverride[] { const defaults = params.cfg.agents?.defaults; - const defaultsRuntime = params.ignoreLegacyAgentRuntimePins ? undefined : defaults?.agentRuntime; + const defaultsRuntime = params.ignoreLegacyAgentRuntimePins + ? undefined + : readLegacyDefaultsRuntime(defaults); const defaultModelRef = readAgentPrimaryModelRef(defaults); const defaultCompaction = asMutableRecord(defaults?.compaction); const hits = collectUnsupportedCodexCompactionOverridesForAgent({ @@ -702,7 +709,7 @@ function getSharedDefaultCompactionOverrideConsumers(params: { if (!hasDefaultModel && !hasDefaultProvider) { return consumers; } - const defaultsRuntime = defaults?.agentRuntime; + const defaultsRuntime = readLegacyDefaultsRuntime(defaults); const inheritedModelRef = readAgentPrimaryModelRef(defaults); const defaultUsesCodexCompaction = agentUsesCodexRuntimeForCompaction({ cfg: params.cfg, @@ -780,7 +787,9 @@ function sharedDefaultLosslessCompactionHasNonCodexConsumer(params: { if (!hasDefaultLosslessProvider && !hasDefaultModel) { return false; } - const defaultsRuntime = params.ignoreLegacyAgentRuntimePins ? undefined : defaults?.agentRuntime; + const defaultsRuntime = params.ignoreLegacyAgentRuntimePins + ? undefined + : readLegacyDefaultsRuntime(defaults); const defaultUsesCodexCompaction = agentUsesCodexRuntimeForCompaction({ cfg: params.cfg, agent: defaults, @@ -903,7 +912,7 @@ function collectAgentModelRefs(params: { function collectConfigModelRefs(cfg: OpenClawConfig, env?: NodeJS.ProcessEnv): CodexRouteHit[] { const hits: CodexRouteHit[] = []; const defaults = cfg.agents?.defaults; - const defaultsRuntime = defaults?.agentRuntime; + const defaultsRuntime = readLegacyDefaultsRuntime(defaults); collectAgentModelRefs({ hits, agent: defaults, @@ -1382,8 +1391,8 @@ function collectCodexRuntimeModelPolicyRefs(params: { if (!trimmed) { continue; } - const runtime = normalizeEmbeddedAgentRuntime( - normalizeString(asMutableRecord(asMutableRecord(entry)?.agentRuntime)?.id), + const runtime = normalizeRuntimeString( + asMutableRecord(asMutableRecord(entry)?.agentRuntime)?.id, ); if (runtime === "codex") { params.refs.push({ path: `${params.path}.${trimmed}`, modelRef: trimmed }); @@ -2214,7 +2223,7 @@ function rewriteConfigModelRefsWithCompactionPolicy(params: { ); const defaultsRuntime = ignoreLegacyAgentRuntimePins ? undefined - : nextConfig.agents?.defaults?.agentRuntime; + : readLegacyDefaultsRuntime(nextConfig.agents?.defaults); const rewrittenInheritedCompactionModels = new Map(); rewriteAgentModelRefs({ cfg: nextConfig, @@ -2408,8 +2417,8 @@ function formatDisabledCodexPluginWarning(params: { blockedOutsideEntry: boolean; }): string { const fixHint = params.blockedOutsideEntry - ? "- Enable plugin loading and remove `codex` from plugins.deny, or set the affected OpenAI models to a PI runtime policy." - : "- Run `openclaw doctor --fix`: it enables plugins.entries.codex, or set the affected OpenAI models to a PI runtime policy."; + ? "- Enable plugin loading and remove `codex` from plugins.deny, or set the affected OpenAI models to an OpenClaw runtime policy." + : "- Run `openclaw doctor --fix`: it enables plugins.entries.codex, or set the affected OpenAI models to an OpenClaw runtime policy."; return [ "- Codex runtime is selected, but the Codex plugin is disabled.", ...params.hits.map( diff --git a/src/commands/doctor/shared/context-engine-host-compat.test.ts b/src/commands/doctor/shared/context-engine-host-compat.test.ts index 72f505f6928..fc715b66924 100644 --- a/src/commands/doctor/shared/context-engine-host-compat.test.ts +++ b/src/commands/doctor/shared/context-engine-host-compat.test.ts @@ -59,14 +59,14 @@ function configWithEngine(engineId: string, cfg: OpenClawConfig = {}): OpenClawC } describe("doctor context-engine host compatibility", () => { - it("collects native Codex and Pi as compatible agent-run hosts", () => { + it("collects native Codex and OpenClaw as compatible agent-run hosts", () => { const hosts = collectConfiguredContextEngineAgentRunHosts({ cfg: { agents: { defaults: { models: { "openai/gpt-5.5": { agentRuntime: { id: "codex" } }, - "anthropic/claude-sonnet-4-6": { agentRuntime: { id: "pi" } }, + "anthropic/claude-sonnet-4-6": { agentRuntime: { id: "openclaw" } }, }, }, }, @@ -75,7 +75,7 @@ describe("doctor context-engine host compatibility", () => { expect(hosts.map((host) => host.host.id).toSorted()).toEqual([ "codex-app-server", - "pi-embedded", + "openclaw-embedded", ]); }); diff --git a/src/commands/doctor/shared/context-engine-host-compat.ts b/src/commands/doctor/shared/context-engine-host-compat.ts index a086c1f5510..652414a0556 100644 --- a/src/commands/doctor/shared/context-engine-host-compat.ts +++ b/src/commands/doctor/shared/context-engine-host-compat.ts @@ -1,16 +1,16 @@ +import { normalizeEmbeddedAgentRuntime } from "../../../agents/agent-runtime-id.js"; import { resolveDefaultAgentDir } from "../../../agents/agent-scope-config.js"; import { resolveCliBackendConfig } from "../../../agents/cli-backends.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../../agents/defaults.js"; import { resolveAgentHarnessPolicy } from "../../../agents/harness/policy.js"; import { getRegisteredAgentHarness } from "../../../agents/harness/registry.js"; -import { normalizeEmbeddedAgentRuntime } from "../../../agents/pi-embedded-runner/runtime.js"; import { normalizeProviderId } from "../../../agents/provider-id.js"; import type { OpenClawConfig } from "../../../config/types.openclaw.js"; import { buildGenericCliContextEngineHostSupport, CODEX_APP_SERVER_CONTEXT_ENGINE_HOST, evaluateContextEngineHostSupport, - PI_EMBEDDED_CONTEXT_ENGINE_HOST, + OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST, type ContextEngineHostSupport, } from "../../../context-engine/host-compat.js"; import { ensureContextEnginesInitialized } from "../../../context-engine/init.js"; @@ -163,8 +163,8 @@ function runtimeHostCandidate(params: { paths: string[]; }): HostCandidate { const runtimeId = normalizeRuntimeId(params.runtimeId) ?? params.runtimeId; - if (runtimeId === "pi" || runtimeId === "auto") { - return { runtimeId, host: PI_EMBEDDED_CONTEXT_ENGINE_HOST, paths: params.paths }; + if (runtimeId === "openclaw" || runtimeId === "auto") { + return { runtimeId, host: OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST, paths: params.paths }; } if (runtimeId === "codex") { return { runtimeId, host: CODEX_APP_SERVER_CONTEXT_ENGINE_HOST, paths: params.paths }; diff --git a/src/commands/doctor/shared/deprecation-compat.test.ts b/src/commands/doctor/shared/deprecation-compat.test.ts index 51245ac6fa9..8b1092d639e 100644 --- a/src/commands/doctor/shared/deprecation-compat.test.ts +++ b/src/commands/doctor/shared/deprecation-compat.test.ts @@ -11,6 +11,7 @@ const datePattern = /^\d{4}-\d{2}-\d{2}$/u; const requiredDoctorCompatCodes = [ "doctor-agent-runtime-embedded-harness", + "doctor-agent-embedded-pi-config", "doctor-plugin-install-config-ledger", "doctor-bundled-plugin-load-paths", "doctor-message-queue-steering-modes", diff --git a/src/commands/doctor/shared/deprecation-compat.ts b/src/commands/doctor/shared/deprecation-compat.ts index fca3d889659..9ff170c0b98 100644 --- a/src/commands/doctor/shared/deprecation-compat.ts +++ b/src/commands/doctor/shared/deprecation-compat.ts @@ -75,11 +75,23 @@ const DOCTOR_DEPRECATION_COMPAT_RECORDS = [ introduced: "2026-04-25", source: "agents.defaults.embeddedHarness; agents.list[].embeddedHarness", migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts", - replacement: "agents.defaults.agentRuntime and agents.list[].agentRuntime", + replacement: "models.providers..agentRuntime or model-scoped agentRuntime", docsPath: "/plugins/sdk-agent-harness", tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], notes: - "Runtime-policy naming changed during the plugin architecture work; verify replacement wording against current agentRuntime docs before removal.", + "Whole-agent runtime pins are retired; doctor preserves intent only when it can move the value to provider/model runtime policy.", + }), + deprecatedCompatRecord({ + code: "doctor-agent-embedded-pi-config", + owner: "agent-runtime", + introduced: "2026-05-21", + source: "agents.defaults.embeddedPi; agents.list[].embeddedPi", + migration: "src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts", + replacement: "agents.defaults.embeddedAgent; agents.list[].embeddedAgent", + docsPath: "/gateway/config-agents", + tests: ["src/commands/doctor/shared/legacy-config-migrate.test.ts"], + notes: + "Runtime code no longer reads the legacy key; doctor keeps this migration only to preserve shipped configs during upgrade.", }), deprecatedCompatRecord({ code: "doctor-agent-sandbox-persession", diff --git a/src/commands/doctor/shared/legacy-config-core-normalizers.ts b/src/commands/doctor/shared/legacy-config-core-normalizers.ts index e24e2abb464..34e99ce9f54 100644 --- a/src/commands/doctor/shared/legacy-config-core-normalizers.ts +++ b/src/commands/doctor/shared/legacy-config-core-normalizers.ts @@ -217,7 +217,7 @@ function resolveLegacyWholeAgentRuntimePolicy(raw: unknown): return undefined; } const runtime = normalizeOptionalLowercaseString(raw.id); - if (!runtime || runtime === "auto" || runtime === "pi") { + if (!runtime || runtime === "auto" || runtime === "openclaw") { return undefined; } const alias = listLegacyRuntimeModelProviderAliases().find( diff --git a/src/commands/doctor/shared/legacy-config-migrate.test.ts b/src/commands/doctor/shared/legacy-config-migrate.test.ts index 2e1b2dbb711..05bfbdcbdfc 100644 --- a/src/commands/doctor/shared/legacy-config-migrate.test.ts +++ b/src/commands/doctor/shared/legacy-config-migrate.test.ts @@ -902,7 +902,7 @@ describe("legacy migrate sandbox scope aliases", () => { list: [ { id: "reviewer", - agentRuntime: { fallback: "pi" }, + agentRuntime: { fallback: "openclaw" }, embeddedHarness: { runtime: "codex", fallback: "none", @@ -985,7 +985,7 @@ describe("legacy migrate sandbox scope aliases", () => { agentRuntime: { id: "claude-cli" }, model: "anthropic/claude-opus-4-7", models: { - "anthropic/claude-opus-4-7": { agentRuntime: { id: "pi" } }, + "anthropic/claude-opus-4-7": { agentRuntime: { id: "openclaw" } }, }, }, }, @@ -997,7 +997,71 @@ describe("legacy migrate sandbox scope aliases", () => { expect(res.config?.agents?.defaults).toEqual({ model: "anthropic/claude-opus-4-7", models: { - "anthropic/claude-opus-4-7": { agentRuntime: { id: "pi" } }, + "anthropic/claude-opus-4-7": { agentRuntime: { id: "openclaw" } }, + }, + }); + }); + + it("moves legacy embeddedPi config into embeddedAgent", () => { + const res = migrateLegacyConfigForTest({ + agents: { + defaults: { + embeddedPi: { + projectSettingsPolicy: "sanitize", + executionContract: "strict-agentic", + }, + }, + list: [ + { + id: "worker", + embeddedPi: { + executionContract: "strict-agentic", + }, + }, + ], + }, + }); + + expect(res.changes).toStrictEqual([ + "Moved agents.defaults.embeddedPi → agents.defaults.embeddedAgent.", + "Moved agents.list.0.embeddedPi → agents.list.0.embeddedAgent.", + ]); + expect(res.config?.agents?.defaults).toEqual({ + embeddedAgent: { + projectSettingsPolicy: "sanitize", + executionContract: "strict-agentic", + }, + }); + expect(res.config?.agents?.list?.[0]).toEqual({ + id: "worker", + embeddedAgent: { + executionContract: "strict-agentic", + }, + }); + }); + + it("merges legacy embeddedPi config without overwriting embeddedAgent", () => { + const res = migrateLegacyConfigForTest({ + agents: { + defaults: { + embeddedAgent: { + executionContract: "default", + }, + embeddedPi: { + projectSettingsPolicy: "sanitize", + executionContract: "strict-agentic", + }, + }, + }, + }); + + expect(res.changes).toStrictEqual([ + "Merged agents.defaults.embeddedPi → agents.defaults.embeddedAgent (filled missing fields from legacy; kept explicit embeddedAgent values).", + ]); + expect(res.config?.agents?.defaults).toEqual({ + embeddedAgent: { + executionContract: "default", + projectSettingsPolicy: "sanitize", }, }); }); @@ -1026,7 +1090,7 @@ describe("legacy migrate sandbox scope aliases", () => { agents: { list: [ { - id: "pi", + id: "openclaw", sandbox: { perSession: false, }, diff --git a/src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts b/src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts index edbe7ba27f0..b7463f37580 100644 --- a/src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts +++ b/src/commands/doctor/shared/legacy-config-migrations.runtime.agents.ts @@ -98,6 +98,21 @@ const LEGACY_AGENT_RUNTIME_POLICY_RULES: LegacyConfigRule[] = [ }, ]; +const DEPRECATED_EMBEDDED_AGENT_KEY_RULES: LegacyConfigRule[] = [ + { + path: ["agents", "defaults", "embeddedPi"], + message: + 'agents.defaults.embeddedPi is legacy; use agents.defaults.embeddedAgent instead. Run "openclaw doctor --fix".', + match: (value) => getRecord(value) !== null, + }, + { + path: ["agents", "list"], + message: + 'agents.list[].embeddedPi is legacy; use agents.list[].embeddedAgent instead. Run "openclaw doctor --fix".', + match: (value) => hasLegacyAgentListEmbeddedAgentKey(value), + }, +]; + const LEGACY_AGENT_LLM_TIMEOUT_RULES: LegacyConfigRule[] = [ { path: ["agents", "defaults", "llm"], @@ -264,6 +279,13 @@ function hasLegacyAgentListEmbeddedHarness(value: unknown): boolean { return value.some((agent) => getRecord(getRecord(agent)?.embeddedHarness) !== null); } +function hasLegacyAgentListEmbeddedAgentKey(value: unknown): boolean { + if (!Array.isArray(value)) { + return false; + } + return value.some((agent) => getRecord(getRecord(agent)?.embeddedPi) !== null); +} + function hasAgentListRuntimePolicy(value: unknown): boolean { if (!Array.isArray(value)) { return false; @@ -289,6 +311,30 @@ function hasAgentListModelTimeout(value: unknown): boolean { }); } +function migrateLegacyEmbeddedAgentKey( + container: Record, + pathLabel: string, + changes: string[], +): void { + const legacy = getRecord(container.embeddedPi); + if (!legacy) { + return; + } + const existing = getRecord(container.embeddedAgent); + if (!existing) { + container.embeddedAgent = legacy; + changes.push(`Moved ${pathLabel}.embeddedPi → ${pathLabel}.embeddedAgent.`); + } else { + const merged = structuredClone(existing); + mergeMissing(merged, legacy); + container.embeddedAgent = merged; + changes.push( + `Merged ${pathLabel}.embeddedPi → ${pathLabel}.embeddedAgent (filled missing fields from legacy; kept explicit embeddedAgent values).`, + ); + } + delete container.embeddedPi; +} + function migrateLegacySandboxPerSession( sandbox: Record, pathLabel: string, @@ -332,7 +378,7 @@ function resolveLegacyAgentRuntimeIntent(raw: unknown): LegacyAgentRuntimeIntent return undefined; } const runtime = typeof record.id === "string" ? record.id.trim().toLowerCase() : ""; - if (!runtime || runtime === "auto" || runtime === "pi") { + if (!runtime || runtime === "auto" || runtime === "openclaw") { return undefined; } const alias = listLegacyRuntimeModelProviderAliases().find( @@ -1106,6 +1152,29 @@ export const LEGACY_CONFIG_MIGRATIONS_RUNTIME_AGENTS: LegacyConfigMigrationSpec[ } }, }), + defineLegacyConfigMigration({ + id: "agents.embeddedPi->embeddedAgent", + describe: "Move legacy embedded agent config key to embeddedAgent", + legacyRules: DEPRECATED_EMBEDDED_AGENT_KEY_RULES, + apply: (raw, changes) => { + const agents = getRecord(raw.agents); + const defaults = getRecord(agents?.defaults); + if (defaults) { + migrateLegacyEmbeddedAgentKey(defaults, "agents.defaults", changes); + } + + if (!Array.isArray(agents?.list)) { + return; + } + for (const [index, agent] of agents.list.entries()) { + const agentRecord = getRecord(agent); + if (!agentRecord) { + continue; + } + migrateLegacyEmbeddedAgentKey(agentRecord, `agents.list.${index}`, changes); + } + }, + }), defineLegacyConfigMigration({ id: "agents.agentRuntime-ignored", describe: "Remove ignored agent-wide runtime policy", diff --git a/src/commands/doctor/shared/missing-configured-plugin-install.ts b/src/commands/doctor/shared/missing-configured-plugin-install.ts index 785207ad57d..34984db0b3a 100644 --- a/src/commands/doctor/shared/missing-configured-plugin-install.ts +++ b/src/commands/doctor/shared/missing-configured-plugin-install.ts @@ -121,7 +121,6 @@ function addConfiguredAgentRuntimePluginIds( ): void { for (const runtime of collectConfiguredRuntimePluginIds(cfg, env ?? process.env, { includeEnvRuntime: false, - includeLegacyAgentRuntimes: false, })) { addConfiguredPluginId(ids, runtime); } diff --git a/src/commands/doctor/shared/plugin-tool-allowlist-warnings.ts b/src/commands/doctor/shared/plugin-tool-allowlist-warnings.ts index 1dafaeb48ea..f51440b8b04 100644 --- a/src/commands/doctor/shared/plugin-tool-allowlist-warnings.ts +++ b/src/commands/doctor/shared/plugin-tool-allowlist-warnings.ts @@ -1,7 +1,7 @@ +import { sanitizeServerName, TOOL_NAME_SEPARATOR } from "../../../agents/agent-bundle-mcp-names.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../../agents/defaults.js"; import { compileGlobPatterns, matchesAnyGlobPattern } from "../../../agents/glob-pattern.js"; import { parseModelRef } from "../../../agents/model-selection-normalize.js"; -import { sanitizeServerName, TOOL_NAME_SEPARATOR } from "../../../agents/pi-bundle-mcp-names.js"; import { normalizeProviderId } from "../../../agents/provider-id.js"; import { mergeAlsoAllowPolicy, diff --git a/src/commands/doctor/shared/stale-plugin-config.test.ts b/src/commands/doctor/shared/stale-plugin-config.test.ts index 926187b3138..4bb16cfc88b 100644 --- a/src/commands/doctor/shared/stale-plugin-config.test.ts +++ b/src/commands/doctor/shared/stale-plugin-config.test.ts @@ -242,7 +242,7 @@ describe("doctor stale plugin config helpers", () => { }, list: [ { - id: "pi", + id: "openclaw", heartbeat: { target: "missing-chat-plugin", }, diff --git a/src/commands/models.list.e2e.test.ts b/src/commands/models.list.e2e.test.ts index d4b81fda8b2..a7b2bfe6dd0 100644 --- a/src/commands/models.list.e2e.test.ts +++ b/src/commands/models.list.e2e.test.ts @@ -86,7 +86,7 @@ vi.mock("../agents/model-catalog.js", () => ({ loadModelCatalog, })); -vi.mock("../agents/pi-embedded-runner/model.js", () => ({ +vi.mock("../agents/embedded-agent-runner/model.js", () => ({ resolveModelWithRegistry: ({ provider, modelId, @@ -98,7 +98,7 @@ vi.mock("../agents/pi-embedded-runner/model.js", () => ({ }) => modelRegistry.find(provider, modelId), })); -vi.mock("../agents/pi-model-discovery.js", () => { +vi.mock("../agents/agent-model-discovery.js", () => { class MockModelRegistry { find(provider: string, id: string) { if (modelRegistryState.findError !== undefined) { diff --git a/src/commands/onboard-auth.test.ts b/src/commands/onboard-auth.test.ts index 6b5ed28ca53..7b81668ba2a 100644 --- a/src/commands/onboard-auth.test.ts +++ b/src/commands/onboard-auth.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { OAuthCredentials } from "@earendil-works/pi-ai"; +import type { OAuthCredentials } from "openclaw/plugin-sdk/llm"; import { afterEach, describe, expect, it, vi } from "vitest"; import { applyAuthProfileConfig, @@ -124,7 +124,6 @@ describe("writeOAuthCredentials", () => { const lifecycle = createAuthTestLifecycle([ "OPENCLAW_STATE_DIR", "OPENCLAW_AGENT_DIR", - "PI_CODING_AGENT_DIR", "OPENCLAW_OAUTH_DIR", ]); @@ -172,7 +171,6 @@ describe("writeOAuthCredentials", () => { await fs.mkdir(workerAgentDir, { recursive: true }); process.env.OPENCLAW_AGENT_DIR = kidAgentDir; - process.env.PI_CODING_AGENT_DIR = kidAgentDir; const creds = { refresh: "refresh-sync", @@ -207,7 +205,6 @@ describe("writeOAuthCredentials", () => { await fs.mkdir(kidAgentDir, { recursive: true }); process.env.OPENCLAW_AGENT_DIR = kidAgentDir; - process.env.PI_CODING_AGENT_DIR = kidAgentDir; const creds = { refresh: "refresh-kid", @@ -275,7 +272,6 @@ describe("upsertApiKeyProfile secret refs", () => { const lifecycle = createAuthTestLifecycle([ "OPENCLAW_STATE_DIR", "OPENCLAW_AGENT_DIR", - "PI_CODING_AGENT_DIR", "MOONSHOT_API_KEY", "OPENAI_API_KEY", "CLOUDFLARE_AI_GATEWAY_API_KEY", @@ -418,11 +414,7 @@ describe("upsertApiKeyProfile secret refs", () => { }); describe("upsertApiKeyProfile", () => { - const lifecycle = createAuthTestLifecycle([ - "OPENCLAW_STATE_DIR", - "OPENCLAW_AGENT_DIR", - "PI_CODING_AGENT_DIR", - ]); + const lifecycle = createAuthTestLifecycle(["OPENCLAW_STATE_DIR", "OPENCLAW_AGENT_DIR"]); afterEach(async () => { await lifecycle.cleanup(); diff --git a/src/commands/onboard-custom-config.ts b/src/commands/onboard-custom-config.ts index f1255113dd9..939f7b07961 100644 --- a/src/commands/onboard-custom-config.ts +++ b/src/commands/onboard-custom-config.ts @@ -16,7 +16,7 @@ import { normalizeAlias } from "./models/alias-name.js"; /** * Wizard default for non-Azure custom APIs when context length is unknown. * Mirrors the generic persisted custom-model catalog fallback and leaves enough - * room above the default compaction reserve floor in `pi-settings.ts`. + * room above the default compaction reserve floor in `agent-settings.ts`. */ export const CUSTOM_PROVIDER_DEFAULT_CONTEXT_WINDOW_TOKENS = 128_000; const DEFAULT_CONTEXT_WINDOW = CUSTOM_PROVIDER_DEFAULT_CONTEXT_WINDOW_TOKENS; diff --git a/src/commands/sessions-cleanup.test.ts b/src/commands/sessions-cleanup.test.ts index 6a1bc1a642c..dd4f19aa2c0 100644 --- a/src/commands/sessions-cleanup.test.ts +++ b/src/commands/sessions-cleanup.test.ts @@ -469,8 +469,8 @@ describe("sessionsCleanupCommand", () => { wouldMutate: true, }, beforeStore: { - stale: { sessionId: "stale", updatedAt: 1, model: "pi:opus" }, - fresh: { sessionId: "fresh", updatedAt: 2, model: "pi:opus" }, + stale: { sessionId: "stale", updatedAt: 1, model: "test:opus" }, + fresh: { sessionId: "fresh", updatedAt: 2, model: "test:opus" }, }, missingKeys: new Set(), staleKeys: new Set(["stale"]), diff --git a/src/commands/sessions.acp-runtime-metadata.test.ts b/src/commands/sessions.acp-runtime-metadata.test.ts index 5e540e254cb..9bd13486699 100644 --- a/src/commands/sessions.acp-runtime-metadata.test.ts +++ b/src/commands/sessions.acp-runtime-metadata.test.ts @@ -4,9 +4,9 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { parseAgentSessionKey } from "../routing/session-key.js"; /** - * Catalog #18 — `openclaw sessions --json` reports `agentRuntime.id: "pi"` for + * Catalog #18 — `openclaw sessions --json` reports `agentRuntime.id: "openclaw"` for * ACP sessions because `resolveAgentRuntimeMetadata` only consults agent-config - * policies (env / agent / defaults / implicit fallback to "pi"). The session + * policies (env / agent / defaults / implicit fallback to "openclaw"). The session * key clearly carries the ACP runtime indicator (the `:acp:` segment), but * `sessions.ts:294` ignores it and just calls `resolveAgentRuntimeMetadata(cfg, agentId)`. * @@ -16,20 +16,20 @@ import { parseAgentSessionKey } from "../routing/session-key.js"; * { * "key": "agent:copilot:acp:86b7b5af-3773-4a56-b244-069d6c5d3db9", * "agentId": "copilot", - * "agentRuntime": { "id": "pi", "source": "implicit" }, + * "agentRuntime": { "id": "openclaw", "source": "implicit" }, * "kind": "direct" * } * - * That is wrong: this session is plainly ACP, not PI. The runtime field is + * That is wrong: this session is plainly ACP, not the native runtime. The runtime field is * supposed to be a faithful classifier of how this session is actually being - * run; instead, every ACP session in the JSON output is mislabelled as `pi`. + * run; instead, every ACP session in the JSON output is mislabelled as the native runtime. * * This test mirrors the exact computation `sessionsCommand` performs at * `src/commands/sessions.ts:294` and proves the bug in two parts: * - * - RED: ACP-keyed session resolves to `id: "pi"`, `source: "implicit"`. + * - RED: ACP-keyed session resolves to `id: "openclaw"`, `source: "implicit"`. * - GREEN control: a non-ACP `agent:main:main` session resolves to the - * same implicit-pi metadata, which IS correct in that case. The control + * same implicit-native metadata, which IS correct in that case. The control * proves the assertion infrastructure is not masking the RED case. * * Fix shape (see the third test): when the session key is ACP-style, @@ -53,7 +53,7 @@ const NON_ACP_SESSION_KEY = "agent:main:main"; * - no top-level `agents.defaults.agentRuntime` either * * Result: `resolveAgentRuntimeMetadata(cfg, "copilot")` falls through to the - * implicit "pi" branch — which is the bug under test. + * implicit "openclaw" branch — which is the bug under test. */ function buildConfigWithoutAgentRuntimePolicy(): OpenClawConfig { return { @@ -112,12 +112,12 @@ describe("sessions --json agentRuntime classifier (catalog #18)", () => { }); // The bug was: the session key plainly contains `:acp:` and yet the - // resolved metadata said id="pi", source="implicit". + // resolved metadata said id="openclaw", source="implicit". // After the fix (applyAcpRuntimeOverlay in resolveModelAgentRuntimeMetadata), // the ACP session key overrides the runtime to id="acpx", source="session-key". expect( agentRuntime.id, - `ACP session ${ACP_SESSION_KEY} should no longer be misclassified as "auto" or "pi". ` + + `ACP session ${ACP_SESSION_KEY} should no longer be misclassified as "auto" or "openclaw". ` + `Got "${agentRuntime.id}". resolveModelAgentRuntimeMetadata must pass sessionKey to ` + `applyAcpRuntimeOverlay so ACP sessions are classified as "acpx".`, ).not.toBe("auto"); @@ -153,7 +153,7 @@ describe("sessions --json agentRuntime classifier (catalog #18)", () => { // // Note: the exact id ("acpx" vs another label) is a design choice for // the fix author. What matters is that it is meaningfully different - // from "pi" and reflects the actual runtime driving the session. + // from "openclaw" and reflects the actual runtime driving the session. // If the fix picks a different label, update this assertion to match — // the structural point (session-key-aware classification) is the // load-bearing part. diff --git a/src/commands/sessions.default-agent-store.test.ts b/src/commands/sessions.default-agent-store.test.ts index e249157d194..4b978bdd1a9 100644 --- a/src/commands/sessions.default-agent-store.test.ts +++ b/src/commands/sessions.default-agent-store.test.ts @@ -35,8 +35,8 @@ function createSessionsConfig(store = "/tmp/sessions-{agentId}.json") { return { agents: { defaults: { - model: { primary: "pi:opus" }, - models: { "pi:opus": {} }, + model: { primary: "test:opus" }, + models: { "test:opus": {} }, contextTokens: 32000, }, list: [ @@ -77,10 +77,10 @@ describe("sessionsCommand default store agent selection", () => { loadSessionStoreMock.mockReset(); loadSessionStoreMock .mockReturnValueOnce({ - main_row: { sessionId: "s1", updatedAt: Date.now() - 60_000, model: "pi:opus" }, + main_row: { sessionId: "s1", updatedAt: Date.now() - 60_000, model: "test:opus" }, }) .mockReturnValueOnce({ - voice_row: { sessionId: "s2", updatedAt: Date.now() - 120_000, model: "pi:opus" }, + voice_row: { sessionId: "s2", updatedAt: Date.now() - 120_000, model: "test:opus" }, }); const { runtime, logs } = createRuntime(); @@ -99,8 +99,8 @@ describe("sessionsCommand default store agent selection", () => { loadConfigMock.mockImplementation(() => createSessionsConfig("/tmp/shared-sessions.json")); loadSessionStoreMock.mockReset(); loadSessionStoreMock.mockReturnValue({ - "agent:main:room": { sessionId: "s1", updatedAt: Date.now() - 60_000, model: "pi:opus" }, - "agent:voice:room": { sessionId: "s2", updatedAt: Date.now() - 30_000, model: "pi:opus" }, + "agent:main:room": { sessionId: "s1", updatedAt: Date.now() - 60_000, model: "test:opus" }, + "agent:voice:room": { sessionId: "s2", updatedAt: Date.now() - 30_000, model: "test:opus" }, }); const { runtime, logs } = createRuntime(); @@ -137,7 +137,7 @@ describe("sessionsCommand default store agent selection", () => { loadSessionStoreMock.mockReset(); loadSessionStoreMock .mockReturnValueOnce({ - main_row: { sessionId: "s1", updatedAt: Date.now() - 60_000, model: "pi:opus" }, + main_row: { sessionId: "s1", updatedAt: Date.now() - 60_000, model: "test:opus" }, }) .mockReturnValueOnce({}); const { runtime, logs } = createRuntime(); diff --git a/src/commands/sessions.model-resolution.test.ts b/src/commands/sessions.model-resolution.test.ts index d87d45bcc5c..bbbab40ea22 100644 --- a/src/commands/sessions.model-resolution.test.ts +++ b/src/commands/sessions.model-resolution.test.ts @@ -55,11 +55,11 @@ describe("sessionsCommand model resolution", () => { { modelProvider: "openai-codex", model: "gpt-5.4", - modelOverride: "pi:opus", + modelOverride: "test:opus", }, "subagent-1", ); - expect(model).toBe("pi:opus"); + expect(model).toBe("test:opus"); }); it("falls back to modelOverride when runtime model is missing", async () => { diff --git a/src/commands/sessions.test-helpers.ts b/src/commands/sessions.test-helpers.ts index c5fe142835f..22177fcdf3a 100644 --- a/src/commands/sessions.test-helpers.ts +++ b/src/commands/sessions.test-helpers.ts @@ -9,8 +9,8 @@ const sessionsConfigState = vi.hoisted<{ loadConfig: () => Record ({ agents: { defaults: { - model: { primary: "pi:opus" }, - models: { "pi:opus": {} }, + model: { primary: "test:opus" }, + models: { "test:opus": {} }, contextTokens: 32000, }, }, diff --git a/src/commands/sessions.test.ts b/src/commands/sessions.test.ts index 7d0990159d9..8091edc898c 100644 --- a/src/commands/sessions.test.ts +++ b/src/commands/sessions.test.ts @@ -36,7 +36,7 @@ describe("sessionsCommand", () => { outputTokens: 800, totalTokens: 2000, totalTokensFresh: true, - model: "pi:opus", + model: "test:opus", }, }); @@ -49,7 +49,7 @@ describe("sessionsCommand", () => { const row = logs.find((line) => line.includes("+15555550123")) ?? ""; expect(row).toBe( - "direct +15555550123 45m ago pi:opus OpenAI Codex 2.0k/32k (6%) id:abc123", + "direct +15555550123 45m ago test:opus OpenAI Codex 2.0k/32k (6%) id:abc123", ); }); @@ -141,7 +141,7 @@ describe("sessionsCommand", () => { const row = logs.find((line) => line.includes("quietchat:group:demo")) ?? ""; expect(row).toBe( - "group quietchat:group:demo 5m ago pi:opus OpenAI Codex unknown/32k (?%) think:high id:xyz", + "group quietchat:group:demo 5m ago test:opus OpenAI Codex unknown/32k (?%) think:high id:xyz", ); }); @@ -154,14 +154,14 @@ describe("sessionsCommand", () => { outputTokens: 800, totalTokens: 2000, totalTokensFresh: true, - model: "pi:opus", + model: "test:opus", }, "quietchat:group:demo": { sessionId: "xyz", updatedAt: Date.now() - 5 * 60_000, inputTokens: 20, outputTokens: 10, - model: "pi:opus", + model: "test:opus", }, }); @@ -187,7 +187,7 @@ describe("sessionsCommand", () => { updatedAt: Date.now() - 10 * 60_000, totalTokens: 2000, totalTokensFresh: false, - model: "pi:opus", + model: "test:opus", }, }); @@ -209,12 +209,12 @@ describe("sessionsCommand", () => { recent: { sessionId: "recent", updatedAt: Date.now() - 5 * 60_000, - model: "pi:opus", + model: "test:opus", }, stale: { sessionId: "stale", updatedAt: Date.now() - 45 * 60_000, - model: "pi:opus", + model: "test:opus", }, }, "sessions-active", @@ -263,9 +263,9 @@ describe("sessionsCommand", () => { it("honors explicit JSON output limits", async () => { const store = writeStore( { - newest: { sessionId: "newest", updatedAt: Date.now(), model: "pi:opus" }, - middle: { sessionId: "middle", updatedAt: Date.now() - 60_000, model: "pi:opus" }, - oldest: { sessionId: "oldest", updatedAt: Date.now() - 120_000, model: "pi:opus" }, + newest: { sessionId: "newest", updatedAt: Date.now(), model: "test:opus" }, + middle: { sessionId: "middle", updatedAt: Date.now() - 60_000, model: "test:opus" }, + oldest: { sessionId: "oldest", updatedAt: Date.now() - 120_000, model: "test:opus" }, }, "sessions-explicit-limit", ); @@ -288,8 +288,8 @@ describe("sessionsCommand", () => { it("allows full JSON output with --limit all", async () => { const store = writeStore( { - newest: { sessionId: "newest", updatedAt: Date.now(), model: "pi:opus" }, - oldest: { sessionId: "oldest", updatedAt: Date.now() - 120_000, model: "pi:opus" }, + newest: { sessionId: "newest", updatedAt: Date.now(), model: "test:opus" }, + oldest: { sessionId: "oldest", updatedAt: Date.now() - 120_000, model: "test:opus" }, }, "sessions-limit-all", ); @@ -312,8 +312,8 @@ describe("sessionsCommand", () => { it("sorts and slices large explicit limits instead of using top-N insertion", async () => { const store = writeStore( { - newest: { sessionId: "newest", updatedAt: Date.now(), model: "pi:opus" }, - oldest: { sessionId: "oldest", updatedAt: Date.now() - 120_000, model: "pi:opus" }, + newest: { sessionId: "newest", updatedAt: Date.now(), model: "test:opus" }, + oldest: { sessionId: "oldest", updatedAt: Date.now() - 120_000, model: "test:opus" }, }, "sessions-large-limit", ); diff --git a/src/commands/sessions.ts b/src/commands/sessions.ts index b4afde57ee9..47d197e23b3 100644 --- a/src/commands/sessions.ts +++ b/src/commands/sessions.ts @@ -205,7 +205,7 @@ function resolveSessionRuntimeLabel(params: { sessionKey: string; }): string { const id = normalizeOptionalLowercaseString(params.agentRuntime.id); - const resolvedHarness = id && id !== "pi" && id !== "auto" ? id : undefined; + const resolvedHarness = id && id !== "openclaw" && id !== "auto" ? id : undefined; return resolveAgentRuntimeLabel({ config: params.cfg, sessionEntry: params.entry, diff --git a/src/commands/status.command-sections.test.ts b/src/commands/status.command-sections.test.ts index e880d67709a..e73907fc47e 100644 --- a/src/commands/status.command-sections.test.ts +++ b/src/commands/status.command-sections.test.ts @@ -81,7 +81,7 @@ describe("status.command-sections", () => { updatedAt: 2, age: 7_000, model: "gpt-5.5", - runtime: "OpenClaw Pi Default", + runtime: "OpenClaw Default", totalTokens: null, totalTokensFresh: false, remainingTokens: null, @@ -116,7 +116,7 @@ describe("status.command-sections", () => { Kind: "cron", Age: "7000ms", Model: "gpt-5.5", - Runtime: "OpenClaw Pi Default", + Runtime: "OpenClaw Default", Tokens: "12k", Cache: "cache ok", }, @@ -147,7 +147,7 @@ describe("status.command-sections", () => { configuredModel: "zhipu/glm-4.5-air", selectedModel: "deepseek/deepseek-v4-flash", modelSelectionReason: "session override", - runtime: "OpenClaw Pi Default", + runtime: "OpenClaw Default", totalTokens: null, totalTokensFresh: false, remainingTokens: null, diff --git a/src/commands/status.command.ts b/src/commands/status.command.ts index 293d7ab7658..3db8f13a3e1 100644 --- a/src/commands/status.command.ts +++ b/src/commands/status.command.ts @@ -31,9 +31,6 @@ const statusAllModuleLoader = createLazyImportLoader(() => import("./status-all. const statusCommandTextRuntimeLoader = createLazyImportLoader( () => import("./status.command.text-runtime.js"), ); -const statusGatewayConnectionRuntimeLoader = createLazyImportLoader( - () => import("./status.gateway-connection.runtime.js"), -); const statusNodeModeModuleLoader = createLazyImportLoader(() => import("./status.node-mode.js")); function loadStatusScanModule() { @@ -52,10 +49,6 @@ function loadStatusCommandTextRuntime() { return statusCommandTextRuntimeLoader.load(); } -function loadStatusGatewayConnectionRuntime() { - return statusGatewayConnectionRuntimeLoader.load(); -} - function loadStatusNodeModeModule() { return statusNodeModeModuleLoader.load(); } @@ -228,7 +221,7 @@ export async function statusCommand( }); if (opts.verbose) { - const { buildGatewayConnectionDetails } = await loadStatusGatewayConnectionRuntime(); + const { buildGatewayConnectionDetails } = await import("../gateway/call.js"); const details = buildGatewayConnectionDetails({ config: scan.cfg }); logGatewayConnectionDetails({ runtime, diff --git a/src/commands/status.gateway-connection.runtime.ts b/src/commands/status.gateway-connection.runtime.ts deleted file mode 100644 index 027ed533c2d..00000000000 --- a/src/commands/status.gateway-connection.runtime.ts +++ /dev/null @@ -1 +0,0 @@ -export { buildGatewayConnectionDetails } from "../gateway/call.js"; diff --git a/src/commands/status.summary.runtime.test.ts b/src/commands/status.summary.runtime.test.ts index 815a5a210b4..859a002ad5a 100644 --- a/src/commands/status.summary.runtime.test.ts +++ b/src/commands/status.summary.runtime.test.ts @@ -96,7 +96,7 @@ describe("statusSummaryRuntime.resolveSessionRuntimeLabel", () => { agents: { defaults: { models: { - "openai/gpt-5.5": { agentRuntime: { id: "pi" } }, + "openai/gpt-5.5": { agentRuntime: { id: "openclaw" } }, }, }, list: [ diff --git a/src/commands/status.summary.runtime.ts b/src/commands/status.summary.runtime.ts index ad17a8df12d..a9653a69b05 100644 --- a/src/commands/status.summary.runtime.ts +++ b/src/commands/status.summary.runtime.ts @@ -168,7 +168,7 @@ function resolveSessionRuntimeLabel(params: { acpBackend: params.entry?.acp?.backend, }); const id = normalizeOptionalLowercaseString(runtime.id); - const resolvedHarness = id && id !== "pi" && id !== "auto" ? id : undefined; + const resolvedHarness = id && id !== "openclaw" && id !== "auto" ? id : undefined; return resolveAgentRuntimeLabel({ config: params.cfg, sessionEntry: params.entry, diff --git a/src/commands/status.summary.test.ts b/src/commands/status.summary.test.ts index 0fa02295586..e930645508d 100644 --- a/src/commands/status.summary.test.ts +++ b/src/commands/status.summary.test.ts @@ -63,7 +63,7 @@ vi.mock("./status.summary.runtime.js", () => ({ provider: "openai", model: "gpt-5.5", })), - resolveSessionRuntimeLabel: vi.fn(() => "OpenClaw Pi Default"), + resolveSessionRuntimeLabel: vi.fn(() => "OpenClaw Default"), resolveContextTokensForModel: vi.fn(() => 200_000), }, })); diff --git a/src/commands/status.test-support.ts b/src/commands/status.test-support.ts index 263d7fe374a..79d2aee377e 100644 --- a/src/commands/status.test-support.ts +++ b/src/commands/status.test-support.ts @@ -119,7 +119,7 @@ const baseStatusSummary = { configuredModel: "openai/gpt-5.5", selectedModel: "openai/gpt-5.5", modelSelectionReason: null, - runtime: "OpenClaw Pi Default", + runtime: "OpenClaw Default", totalTokens: 12_000, totalTokensFresh: true, remainingTokens: 4_000, diff --git a/src/commands/status.test.ts b/src/commands/status.test.ts index dddf36a2320..064625dd25f 100644 --- a/src/commands/status.test.ts +++ b/src/commands/status.test.ts @@ -27,7 +27,7 @@ function createDefaultSessionStoreEntry() { totalTokens: 5_000, totalTokensFresh: true as boolean, contextTokens: 10_000, - model: "pi:opus", + model: "test:opus", sessionId: "abc123", systemSent: true, }; @@ -40,7 +40,7 @@ function createUnknownUsageSessionStore() { inputTokens: 2_000, outputTokens: 3_000, contextTokens: 10_000, - model: "pi:opus", + model: "test:opus", }, }; } @@ -262,7 +262,7 @@ function createSessionStatusRows() { paths: byAgent.map((entry) => entry.path), count: recent.length, defaults: { - model: recent[0]?.model ?? "pi:opus", + model: recent[0]?.model ?? "test:opus", contextTokens: recent[0]?.contextTokens ?? 10_000, }, recent, @@ -1006,7 +1006,7 @@ describe("statusCommand", () => { expect(payload.memoryPlugin.slot).toBe("memory-core"); expect(payload.sessions.count).toBe(1); expect(payload.sessions.paths).toContain("/tmp/sessions.json"); - expect(payload.sessions.defaults.model).toBe("pi:opus"); + expect(payload.sessions.defaults.model).toBe("test:opus"); expect(payload.sessions.defaults.contextTokens).toBeGreaterThan(0); expect(payload.sessions.recent[0].percentUsed).toBe(50); expect(payload.sessions.recent[0].cacheRead).toBe(2_000); @@ -1106,7 +1106,7 @@ describe("statusCommand", () => { totalTokens: 5_000, totalTokensFresh: false, contextTokens: 10_000, - model: "pi:opus", + model: "test:opus", }, }); runtimeLogMock.mockClear(); @@ -1466,7 +1466,7 @@ describe("statusCommand", () => { outputTokens: 1_000, totalTokens: 2_000, contextTokens: 10_000, - model: "pi:opus", + model: "test:opus", }, }; } diff --git a/src/commitments/runtime.test.ts b/src/commitments/runtime.test.ts index 330994d72d5..ddec4cf8b56 100644 --- a/src/commitments/runtime.test.ts +++ b/src/commitments/runtime.test.ts @@ -13,29 +13,29 @@ import { import { loadCommitmentStore } from "./store.js"; import type { CommitmentExtractionBatchResult, CommitmentExtractionItem } from "./types.js"; -const runEmbeddedPiAgentMock = vi.hoisted(() => vi.fn()); +const runEmbeddedAgentMock = vi.hoisted(() => vi.fn()); const resolveDefaultModelMock = vi.hoisted(() => vi.fn()); -vi.mock("../agents/pi-embedded.js", () => ({ - runEmbeddedPiAgent: runEmbeddedPiAgentMock, +vi.mock("../agents/embedded-agent.js", () => ({ + runEmbeddedAgent: runEmbeddedAgentMock, })); vi.mock("./model-selection.runtime.js", () => ({ resolveCommitmentDefaultModelRef: resolveDefaultModelMock, })); -function requireFirstEmbeddedPiRequest(): { +function requireFirstEmbeddedAgentRequest(): { provider?: string; model?: string; disableTools?: boolean; } { - const [call] = runEmbeddedPiAgentMock.mock.calls; + const [call] = runEmbeddedAgentMock.mock.calls; if (!call) { - throw new Error("expected embedded PI agent extraction request"); + throw new Error("expected embedded OpenClaw agent extraction request"); } const [request] = call; if (!request || typeof request !== "object" || Array.isArray(request)) { - throw new Error("expected embedded PI agent extraction request"); + throw new Error("expected embedded OpenClaw agent extraction request"); } return request as { provider?: string; model?: string; disableTools?: boolean }; } @@ -46,7 +46,7 @@ describe("commitment extraction runtime", () => { afterEach(async () => { resetCommitmentExtractionRuntimeForTests(); - runEmbeddedPiAgentMock.mockReset(); + runEmbeddedAgentMock.mockReset(); resolveDefaultModelMock.mockReset(); vi.useRealTimers(); vi.unstubAllEnvs(); @@ -192,7 +192,7 @@ describe("commitment extraction runtime", () => { }, }, }; - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: '{"candidates":[]}' }], }); resolveDefaultModelMock.mockReturnValue({ @@ -219,8 +219,8 @@ describe("commitment extraction runtime", () => { await expect(drainCommitmentExtractionQueue()).resolves.toBe(1); expect(resolveDefaultModelMock).toHaveBeenCalledWith({ cfg, agentId: "main" }); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); - const request = requireFirstEmbeddedPiRequest(); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); + const request = requireFirstEmbeddedAgentRequest(); expect(request.provider).toBe("openai-codex"); expect(request.model).toBe("gpt-5.5"); expect(request.disableTools).toBe(true); diff --git a/src/commitments/runtime.ts b/src/commitments/runtime.ts index 51d6abc13fb..6ced378210b 100644 --- a/src/commitments/runtime.ts +++ b/src/commitments/runtime.ts @@ -20,7 +20,7 @@ import type { type TimerHandle = ReturnType; type ModelRef = { provider: string; model: string }; -type EmbeddedPiPayloadResult = { payloads?: Array<{ text?: string }> }; +type EmbeddedAgentPayloadResult = { payloads?: Array<{ text?: string }> }; type CommitmentExtractionEnqueueInput = CommitmentScope & { cfg?: OpenClawConfig; @@ -191,7 +191,7 @@ function resolveExtractionSessionFile(agentId: string, runId: string): string { ); } -function joinPayloadText(result: EmbeddedPiPayloadResult): string { +function joinPayloadText(result: EmbeddedAgentPayloadResult): string { return ( result.payloads ?.map((payload) => payload.text) @@ -224,8 +224,8 @@ async function defaultExtractBatch(params: { const resolved = resolveCommitmentsConfig(cfg); const runId = `commitments-${randomUUID()}`; const modelRef = await resolveDefaultModel({ cfg, agentId: first.agentId }); - const { runEmbeddedPiAgent } = await import("../agents/pi-embedded.js"); - const result = await runEmbeddedPiAgent({ + const { runEmbeddedAgent } = await import("../agents/embedded-agent.js"); + const result = await runEmbeddedAgent({ sessionId: runId, sessionKey: `agent:${first.agentId}:commitments:${runId}`, agentId: first.agentId, diff --git a/src/config/config-misc.test.ts b/src/config/config-misc.test.ts index 667a9c3064d..6c4d65dc692 100644 --- a/src/config/config-misc.test.ts +++ b/src/config/config-misc.test.ts @@ -1143,7 +1143,7 @@ describe("config paths", () => { describe("config strict validation", () => { it("rejects unknown fields", () => { const res = validateConfigObject({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, customUnknownField: { nested: "value" }, }); expect(res.ok).toBe(false); @@ -1317,7 +1317,7 @@ describe("config strict validation", () => { }, list: [ { - id: "pi", + id: "openclaw", sandbox: { perSession: false, }, diff --git a/src/config/config.compaction-settings.test.ts b/src/config/config.compaction-settings.test.ts index 7c12fa09bd9..0b0fae2c95f 100644 --- a/src/config/config.compaction-settings.test.ts +++ b/src/config/config.compaction-settings.test.ts @@ -56,7 +56,7 @@ describe("config compaction settings", () => { expect(compaction?.maxActiveTranscriptBytes).toBe("20mb"); }); - it("preserves pi compaction override values", () => { + it("preserves legacy compaction override values", () => { const compaction = materializeCompactionConfig({ reserveTokens: 15_000, keepRecentTokens: 12_000, diff --git a/src/config/config.hooks-module-paths.test.ts b/src/config/config.hooks-module-paths.test.ts index 4d1411fd67b..40a3ff84231 100644 --- a/src/config/config.hooks-module-paths.test.ts +++ b/src/config/config.hooks-module-paths.test.ts @@ -14,7 +14,7 @@ describe("config hooks module paths", () => { it("rejects absolute hooks.mappings[].transform.module", () => { expectRejectedIssuePath( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, hooks: { mappings: [ { @@ -32,7 +32,7 @@ describe("config hooks module paths", () => { it("rejects escaping hooks.mappings[].transform.module", () => { expectRejectedIssuePath( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, hooks: { mappings: [ { @@ -50,7 +50,7 @@ describe("config hooks module paths", () => { it("rejects absolute hooks.internal.handlers[].module", () => { expectRejectedIssuePath( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, hooks: { internal: { enabled: true, @@ -65,7 +65,7 @@ describe("config hooks module paths", () => { it("rejects escaping hooks.internal.handlers[].module", () => { expectRejectedIssuePath( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, hooks: { internal: { enabled: true, @@ -79,7 +79,7 @@ describe("config hooks module paths", () => { it("accepts hooks.mappings[].channel runtime plugin ids", () => { const res = validateConfigObjectWithPlugins({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, hooks: { mappings: [ { @@ -97,7 +97,7 @@ describe("config hooks module paths", () => { it("rejects blank hooks.mappings[].channel values", () => { expectRejectedIssuePath( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, hooks: { mappings: [ { diff --git a/src/config/config.plugin-validation.test.ts b/src/config/config.plugin-validation.test.ts index 92b2a42c803..5303370bc39 100644 --- a/src/config/config.plugin-validation.test.ts +++ b/src/config/config.plugin-validation.test.ts @@ -141,7 +141,7 @@ describe("config plugin validation", () => { const validateRemovedPluginConfig = (removedId: string) => validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: false, entries: { [removedId]: { enabled: true } }, @@ -245,7 +245,7 @@ describe("config plugin validation", () => { it("reports missing plugin refs across entries and allowlist surfaces", () => { const missingPath = path.join(suiteHome, "missing-plugin-dir"); const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [missingPath] }, @@ -299,7 +299,7 @@ describe("config plugin validation", () => { it("warns instead of failing for stale plugins.deny entries", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { deny: ["missing-deny"], }, @@ -318,7 +318,7 @@ describe("config plugin validation", () => { it("deduplicates catalog install hints for missing configured official external plugins", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { entries: { brave: { enabled: true } }, allow: ["brave"], @@ -352,7 +352,7 @@ describe("config plugin validation", () => { it("warns instead of failing when an official external memory slot plugin is not installed", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { slots: { memory: "memory-lancedb" }, entries: { "memory-lancedb": { enabled: true } }, @@ -381,7 +381,7 @@ describe("config plugin validation", () => { it("keeps no-persistent-memory wording scoped to the selected missing memory slot", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { slots: { memory: "none" }, entries: { "memory-lancedb": { enabled: true } }, @@ -414,7 +414,7 @@ describe("config plugin validation", () => { it("deduplicates yuanbao missing-plugin warnings across entries and allow", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { entries: { yuanbao: { enabled: true } }, allow: ["yuanbao"], @@ -441,7 +441,7 @@ describe("config plugin validation", () => { it("keeps official external non-memory plugins fatal in the memory slot", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { slots: { memory: "brave" }, entries: { brave: { enabled: true } }, @@ -473,7 +473,7 @@ describe("config plugin validation", () => { it("keeps blocked official external memory slot plugins fatal", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { slots: { memory: "memory-lancedb" }, entries: { "memory-lancedb": { enabled: true } }, @@ -523,7 +523,7 @@ describe("config plugin validation", () => { await fs.chmod(blockedPluginDir, 0o777); try { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [blockedPluginDir] }, @@ -562,7 +562,7 @@ describe("config plugin validation", () => { it("maps legacy blocked diagnostics without plugin ids to configured load paths", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [blockedPluginDir] }, @@ -609,7 +609,7 @@ describe("config plugin validation", () => { it("warns for broken discovered plugins that are not referenced by config", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { allow: ["telegram"], }, @@ -647,7 +647,7 @@ describe("config plugin validation", () => { it("keeps broken discovered plugins fatal when config references them", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { entries: { "broken-local": { enabled: true }, @@ -687,7 +687,7 @@ describe("config plugin validation", () => { const aliasDir = path.join(suiteHome, "alias-dir"); const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [aliasDir] }, @@ -743,7 +743,7 @@ describe("config plugin validation", () => { it("warns instead of failing for stale channel config backed by missing plugin refs", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, channels: { "missing-chat": { token: "stale" }, }, @@ -776,7 +776,7 @@ describe("config plugin validation", () => { it("keeps unknown channel typos fatal when there is no stale plugin evidence", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, channels: { telegarm: { botToken: "typo" }, }, @@ -801,7 +801,7 @@ describe("config plugin validation", () => { it("warns when plugins.allow contains a channel id without a plugin manifest (#76872)", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, channels: { discord: { token: "xxx" }, }, @@ -853,7 +853,7 @@ describe("config plugin validation", () => { ); try { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, channels: { "missing-sms": { token: "stale" }, }, @@ -875,7 +875,7 @@ describe("config plugin validation", () => { it("warns with actionable guidance when a runtime command name is used in plugins.allow", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { allow: ["dreaming"], entries: { @@ -905,7 +905,7 @@ describe("config plugin validation", () => { it("does not fail validation for the implicit default memory slot when plugins config is explicit", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { entries: { acpx: { enabled: true } }, }, @@ -961,7 +961,7 @@ describe("config plugin validation", () => { it("surfaces plugin config diagnostics", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [badPluginDir] }, @@ -982,7 +982,7 @@ describe("config plugin validation", () => { it("surfaces invalid Codex native plugin marketplaces as config diagnostics", () => { const res = validateConfigObjectWithPlugins( { - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { entries: { codex: { @@ -1031,7 +1031,7 @@ describe("config plugin validation", () => { it("does not require native config schemas for enabled bundle plugins", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [bundlePluginDir] }, @@ -1044,7 +1044,7 @@ describe("config plugin validation", () => { it("accepts enabled manifestless Claude bundles without a native schema", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [manifestlessClaudeBundleDir] }, @@ -1057,7 +1057,7 @@ describe("config plugin validation", () => { it("surfaces allowed enum values for plugin config diagnostics", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [enumPluginDir] }, @@ -1077,7 +1077,7 @@ describe("config plugin validation", () => { it("accepts voice-call webhookSecurity and streaming guard config fields", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [voiceCallSchemaPluginDir] }, @@ -1108,7 +1108,7 @@ describe("config plugin validation", () => { it("accepts voice-call OpenAI TTS speed, instructions, and baseUrl config fields", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [voiceCallSchemaPluginDir] }, @@ -1135,7 +1135,7 @@ describe("config plugin validation", () => { it("accepts voice-call SecretRef credentials declared by the plugin schema", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [voiceCallSchemaPluginDir] }, @@ -1167,7 +1167,7 @@ describe("config plugin validation", () => { it("rejects out-of-range voice-call OpenAI TTS speed values", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [voiceCallSchemaPluginDir] }, @@ -1200,7 +1200,7 @@ describe("config plugin validation", () => { it("rejects out-of-range voice-call ElevenLabs voice settings", () => { const res = validateInSuite({ - agents: { list: [{ id: "pi" }] }, + agents: { list: [{ id: "openclaw" }] }, plugins: { enabled: true, load: { paths: [voiceCallSchemaPluginDir] }, @@ -1237,7 +1237,7 @@ describe("config plugin validation", () => { const res = validateInSuite({ agents: { defaults: { heartbeat: { target: "last", directPolicy: "block" } }, - list: [{ id: "pi", heartbeat: { directPolicy: "allow" } }], + list: [{ id: "openclaw", heartbeat: { directPolicy: "allow" } }], }, channels: { modelByChannel: { @@ -1253,7 +1253,7 @@ describe("config plugin validation", () => { it("accepts plugin heartbeat targets", () => { const res = validateInSuite({ - agents: { defaults: { heartbeat: { target: "chat" } }, list: [{ id: "pi" }] }, + agents: { defaults: { heartbeat: { target: "chat" } }, list: [{ id: "openclaw" }] }, plugins: { enabled: false, load: { paths: [chatPluginDir] } }, }); expect(res.ok).toBe(true); @@ -1270,7 +1270,7 @@ describe("config plugin validation", () => { const res = validateInSuite({ agents: { defaults: { heartbeat: { target: "not-a-channel" } }, - list: [{ id: "pi" }], + list: [{ id: "openclaw" }], }, }); expect(res.ok).toBe(false); @@ -1290,7 +1290,7 @@ describe("config plugin validation", () => { const res = validateInSuite({ agents: { defaults: { heartbeat: { directPolicy: "maybe" } }, - list: [{ id: "pi" }], + list: [{ id: "openclaw" }], }, }); expect(res.ok).toBe(false); diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 56ec254857c..7e69057c539 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -26,7 +26,7 @@ type ProviderPolicyDefaultsOptions = { let defaultWarnState: WarnState = { warned: false }; const DEFAULT_MODEL_ALIASES: Readonly> = { - // Anthropic (pi-ai catalog uses "latest" ids without date suffix) + // Anthropic (shared model runtime catalog uses "latest" ids without date suffix) opus: "anthropic/claude-opus-4-7", sonnet: "anthropic/claude-sonnet-4-6", diff --git a/src/config/plugin-auto-enable.core.test.ts b/src/config/plugin-auto-enable.core.test.ts index c8e22906ed5..9019280fee8 100644 --- a/src/config/plugin-auto-enable.core.test.ts +++ b/src/config/plugin-auto-enable.core.test.ts @@ -620,8 +620,12 @@ describe("applyPluginAutoEnable core", () => { agents: { defaults: { model: "openai/gpt-5.5", - agentRuntime: { - id: "codex", + models: { + "openai/gpt-5.5": { + agentRuntime: { + id: "codex", + }, + }, }, }, }, @@ -835,7 +839,7 @@ describe("applyPluginAutoEnable core", () => { }, }, agents: { - list: [{ id: "pi" }], + list: [{ id: "openclaw" }], }, }, env, @@ -849,7 +853,7 @@ describe("applyPluginAutoEnable core", () => { }, }, agents: { - list: [{ id: "pi" }], + list: [{ id: "openclaw" }], }, }); expect(result.changes).toStrictEqual([]); diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index ba221dd5090..ffeffa7d63d 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -424,11 +424,11 @@ export const FIELD_HELP: Record = { "tools.experimental": "Experimental built-in tool flags. Keep these off by default and enable only when you are intentionally testing a preview surface.", "tools.experimental.planTool": - "Enable the experimental structured `update_plan` tool for non-trivial multi-step work tracking. Leave this off unless you explicitly want the tool outside strict-agentic embedded Pi runs.", + "Enable the experimental structured `update_plan` tool for non-trivial multi-step work tracking. Leave this off unless you explicitly want the tool outside strict-agentic embedded OpenClaw runs.", "tools.toolSearch": "Compact large OpenClaw, MCP, and client tool catalogs behind one search/call surface. Set to true for the default code bridge or use the object form to choose the structured fallback.", "tools.toolSearch.enabled": - "Enables Tool Search. When on, OpenClaw hides large tool catalogs behind `tool_search_code` or structured search/describe/call tools during PI runs.", + "Enables Tool Search. When on, OpenClaw hides large tool catalogs behind `tool_search_code` or structured search/describe/call tools during embedded runtime runs.", "tools.toolSearch.mode": 'Choose the model-facing surface: "code" exposes `tool_search_code`; "tools" exposes structured search/describe/call fallback tools.', "tools.toolSearch.codeTimeoutMs": @@ -968,7 +968,7 @@ export const FIELD_HELP: Record = { "models.providers.*.agentRuntime": "Optional low-level agent runtime policy for this provider. Use provider/model runtime policy instead of agent-wide runtime pins; omitted/default lets OpenClaw choose the runtime for the selected provider.", "models.providers.*.agentRuntime.id": - 'Provider agent runtime id: "pi", "auto", a registered plugin harness id such as "codex", or a supported CLI backend alias such as "claude-cli". OpenAI on the official endpoint defaults to the Codex harness when omitted.', + 'Provider agent runtime id: "openclaw", "auto", a registered plugin harness id such as "codex", or a supported CLI backend alias such as "claude-cli". OpenAI on the official endpoint defaults to the Codex harness when omitted.', "models.providers.*.localService": "Optional on-demand local model server process for this provider. OpenClaw probes healthUrl, starts the command when needed, waits for readiness, and then sends the model request.", "models.providers.*.localService.command": @@ -1053,7 +1053,7 @@ export const FIELD_HELP: Record = { "models.providers.*.models[].agentRuntime": "Optional low-level agent runtime policy for this specific model. Model runtime policy overrides the provider runtime policy.", "models.providers.*.models[].agentRuntime.id": - 'Model agent runtime id: "pi", "auto", a registered plugin harness id such as "codex", or a supported CLI backend alias such as "claude-cli".', + 'Model agent runtime id: "openclaw", "auto", a registered plugin harness id such as "codex", or a supported CLI backend alias such as "claude-cli".', "models.providers.*.models[].mediaInput": "Optional model media capability metadata used by tools to choose conservative image compression defaults.", "models.providers.*.models[].mediaInput.image": @@ -1148,7 +1148,7 @@ export const FIELD_HELP: Record = { "agents.defaults.models.*.agentRuntime": "Optional per-model runtime policy for the default agent. Use this for model-specific runtime exceptions instead of setting a whole-agent runtime.", "agents.defaults.models.*.agentRuntime.id": - 'Default-agent model runtime id: "pi", "auto", a registered plugin harness id such as "codex", or a supported CLI backend alias such as "claude-cli".', + 'Default-agent model runtime id: "openclaw", "auto", a registered plugin harness id such as "codex", or a supported CLI backend alias such as "claude-cli".', "agents.defaults.memorySearch": "Vector search over MEMORY.md and memory/*.md (per-agent overrides supported).", "agents.defaults.memorySearch.enabled": @@ -1396,27 +1396,11 @@ export const FIELD_HELP: Record = { "agents.defaults.model.primary": "Primary model (provider/model).", "agents.defaults.model.fallbacks": "Ordered fallback models (provider/model). Used when the primary model fails.", - "agents.defaults.agentRuntime": - "Legacy whole-agent runtime policy. It is ignored by runtime selection; configure runtime policy on a provider or model instead. Run openclaw doctor --fix to remove stale values.", - "agents.defaults.agentRuntime.id": - "Legacy whole-agent runtime id. It is ignored by runtime selection; configure models.providers..agentRuntime.id or a model-specific agentRuntime.id instead.", - "agents.defaults.embeddedHarness": - "Legacy whole-agent embedded harness input. Run openclaw doctor --fix to remove it and use provider/model runtime policy where needed.", - "agents.defaults.embeddedHarness.runtime": - "Legacy whole-agent embedded harness runtime. Runtime selection ignores it; use provider/model runtime policy.", "agents.list.*.models": "Per-agent model catalog overrides keyed by full provider/model IDs.", "agents.list.*.models.*.agentRuntime": "Optional per-model runtime policy for this agent. Use this for agent-specific model exceptions instead of setting a whole-agent runtime.", "agents.list.*.models.*.agentRuntime.id": - 'Per-agent model runtime id: "pi", "auto", a registered plugin harness id such as "codex", or a supported CLI backend alias such as "claude-cli".', - "agents.list.*.agentRuntime": - "Legacy per-agent runtime policy. It is ignored by runtime selection; configure provider/model runtime policy instead. Run openclaw doctor --fix to remove stale values.", - "agents.list.*.agentRuntime.id": - "Legacy per-agent runtime id. It is ignored by runtime selection; configure a provider/model runtime id instead.", - "agents.list.*.embeddedHarness": - "Legacy per-agent embedded harness input. Run openclaw doctor --fix to remove it and use provider/model runtime policy where needed.", - "agents.list.*.embeddedHarness.runtime": - "Legacy per-agent embedded harness runtime. Runtime selection ignores it; use provider/model runtime policy.", + 'Per-agent model runtime id: "openclaw", "auto", a registered plugin harness id such as "codex", or a supported CLI backend alias such as "claude-cli".', "agents.defaults.imageModel.primary": "Optional image model (provider/model) used when the primary model lacks image input.", "agents.defaults.imageModel.fallbacks": "Ordered fallback image models (provider/model).", @@ -1461,7 +1445,7 @@ export const FIELD_HELP: Record = { "agents.defaults.compaction.keepRecentTokens": "Minimum token budget preserved from the most recent conversation window during compaction. Use higher values to protect immediate context continuity and lower values to keep more long-tail history.", "agents.defaults.compaction.reserveTokensFloor": - "Minimum floor enforced for reserveTokens in Pi compaction paths (0 disables the floor guard). Use a non-zero floor to avoid over-aggressive compression under fluctuating token estimates.", + "Minimum floor enforced for reserveTokens in embedded OpenClaw compaction paths (0 disables the floor guard). Use a non-zero floor to avoid over-aggressive compression under fluctuating token estimates.", "agents.defaults.compaction.maxHistoryShare": "Maximum fraction of total context budget allowed for retained history after compaction (range 0.1-0.9). Use lower shares for more generation headroom or higher shares for deeper historical continuity.", "agents.defaults.compaction.identifierPolicy": @@ -1477,9 +1461,9 @@ export const FIELD_HELP: Record = { "agents.defaults.compaction.qualityGuard.maxRetries": "Maximum number of regeneration retries after a failed safeguard summary quality audit. Use small values to bound extra latency and token cost.", "agents.defaults.compaction.midTurnPrecheck": - "Optional Pi tool-loop precheck that detects context pressure after a tool result is appended and before the next model call. When enabled, OpenClaw reuses existing precheck recovery to truncate tool results or compact before retrying.", + "Optional embedded OpenClaw tool-loop precheck that detects context pressure after a tool result is appended and before the next model call. When enabled, OpenClaw reuses existing precheck recovery to truncate tool results or compact before retrying.", "agents.defaults.compaction.midTurnPrecheck.enabled": - "Enable structured mid-turn context pressure checks for Pi tool loops. Default: false. Keep disabled unless long tool-heavy sessions hit context overflow before normal turn-end compaction can run.", + "Enable structured mid-turn context pressure checks for embedded OpenClaw tool loops. Default: false. Keep disabled unless long tool-heavy sessions hit context overflow before normal turn-end compaction can run.", "agents.defaults.compaction.postIndexSync": 'Controls post-compaction session memory reindex mode: "off", "async", or "await" (default: "async"). Use "await" for strongest freshness, "async" for lower compaction latency, and "off" only when session-memory sync is handled elsewhere.', "agents.defaults.compaction.postCompactionSections": @@ -1509,9 +1493,9 @@ export const FIELD_HELP: Record = { "agents.defaults.compaction.memoryFlush.systemPrompt": "System-prompt override for the pre-compaction memory flush turn to control extraction style and safety constraints. Use carefully so custom instructions do not reduce memory quality or leak sensitive context.", "agents.defaults.runRetries": - "Outer run loop retry iteration boundaries for the embedded Pi runner to prevent infinite execution loops during failure recovery.", + "Outer run loop retry iteration boundaries for the embedded OpenClaw runner to prevent infinite execution loops during failure recovery.", "agents.defaults.runRetries.base": - "Base number of run retry iterations for the embedded Pi runner's outer run loop (default: 24).", + "Base number of run retry iterations for the embedded OpenClaw runner's outer run loop (default: 24).", "agents.defaults.runRetries.perProfile": "Additional run retry iterations granted per fallback profile candidate (default: 8).", "agents.defaults.runRetries.min": @@ -1519,22 +1503,22 @@ export const FIELD_HELP: Record = { "agents.defaults.runRetries.max": "Maximum absolute limit for run retry iterations to prevent runaway execution (default: 160).", "agents.list[].runRetries": - "Optional per-agent override for the embedded Pi runner's outer run loop retry iteration boundaries.", + "Optional per-agent override for the embedded OpenClaw runner's outer run loop retry iteration boundaries.", "agents.list[].runRetries.base": "Base number of run retry iterations for this agent.", "agents.list[].runRetries.perProfile": "Additional run retry iterations granted per fallback profile candidate for this agent.", "agents.list[].runRetries.min": "Minimum absolute limit for run retry iterations for this agent.", "agents.list[].runRetries.max": "Maximum absolute limit for run retry iterations for this agent.", - "agents.defaults.embeddedPi": - "Embedded Pi runner hardening controls for how workspace-local Pi settings are trusted and applied in OpenClaw sessions.", - "agents.defaults.embeddedPi.projectSettingsPolicy": - 'How embedded Pi handles workspace-local `.pi/config/settings.json`: "sanitize" (default) strips shellPath/shellCommandPrefix, "ignore" disables project settings entirely, and "trusted" applies project settings as-is.', - "agents.defaults.embeddedPi.executionContract": - 'Embedded Pi execution contract: "default" keeps the standard runner behavior, while "strict-agentic" keeps OpenAI/OpenAI Codex GPT-5-family runs acting until they hit a real blocker instead of stopping at plans or filler.', - "agents.list[].embeddedPi": - "Optional per-agent embedded Pi overrides. Use this to opt specific agents into stricter GPT-5 execution behavior without changing the global default.", - "agents.list[].embeddedPi.executionContract": - 'Optional per-agent embedded Pi execution contract override. Set "strict-agentic" to keep that agent acting through plan-only turns on OpenAI/OpenAI Codex GPT-5-family runs, or "default" to inherit the standard runner behavior.', + "agents.defaults.embeddedAgent": + "Embedded OpenClaw runner hardening controls for how workspace-local agent settings are trusted and applied in OpenClaw sessions.", + "agents.defaults.embeddedAgent.projectSettingsPolicy": + 'How embedded OpenClaw handles workspace-local `.openclaw/settings.json`: "sanitize" (default) strips shellPath/shellCommandPrefix, "ignore" disables project settings entirely, and "trusted" applies project settings as-is.', + "agents.defaults.embeddedAgent.executionContract": + 'Embedded OpenClaw execution contract: "default" keeps the standard runner behavior, while "strict-agentic" keeps OpenAI/OpenAI Codex GPT-5-family runs acting until they hit a real blocker instead of stopping at plans or filler.', + "agents.list[].embeddedAgent": + "Optional per-agent embedded OpenClaw overrides. Use this to opt specific agents into stricter GPT-5 execution behavior without changing the global default.", + "agents.list[].embeddedAgent.executionContract": + 'Optional per-agent embedded OpenClaw execution contract override. Set "strict-agentic" to keep that agent acting through plan-only turns on OpenAI/OpenAI Codex GPT-5-family runs, or "default" to inherit the standard runner behavior.', "agents.defaults.humanDelay.mode": 'Delay style for block replies ("off", "natural", "custom").', "agents.defaults.humanDelay.minMs": "Minimum delay in ms for custom humanDelay (default: 800).", "agents.defaults.humanDelay.maxMs": "Maximum delay in ms for custom humanDelay (default: 2500).", @@ -1566,7 +1550,7 @@ export const FIELD_HELP: Record = { "Optional secret used to HMAC hash owner IDs when ownerDisplay=hash. Prefer env substitution.", "commands.allowFrom": "Defines elevated command allow rules by channel and sender for owner-level command surfaces. Use narrow provider-specific identities so privileged commands are not exposed to broad chat audiences.", - mcp: "Global MCP server definitions managed by OpenClaw. Embedded Pi and other runtime adapters can consume these servers without storing them inside Pi-owned project settings.", + mcp: "Global MCP server definitions managed by OpenClaw. Embedded OpenClaw and other runtime adapters can consume these servers without storing them inside runtime-owned project settings.", "mcp.servers": "Named MCP server definitions. OpenClaw stores them in its own config and runtime adapters decide which transports are supported at execution time.", "mcp.servers.*.codex": diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index 278f6497ed7..2035abe7456 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -93,10 +93,6 @@ export const FIELD_LABELS: Record = { "agents.defaults.contextLimits.memoryGetDefaultLines": "Default memory_get Line Window", "agents.defaults.contextLimits.toolResultMaxChars": "Default Tool Result Max Chars", "agents.defaults.contextLimits.postCompactionMaxChars": "Default Post-compaction Max Chars", - "agents.defaults.agentRuntime": "Legacy Default Agent Runtime", - "agents.defaults.agentRuntime.id": "Legacy Default Agent Runtime ID", - "agents.defaults.embeddedHarness": "Default Legacy Embedded Harness Settings", - "agents.defaults.embeddedHarness.runtime": "Default Legacy Embedded Harness Runtime", "agents.list": "Agent List", "agents.list[].skillsLimits": "Agent Skills Limits", "agents.list[].skillsLimits.maxSkillsPromptChars": "Agent Skills Prompt Max Chars", @@ -110,8 +106,6 @@ export const FIELD_LABELS: Record = { "agents.list.*.models.*.agentRuntime.id": "Agent Model Runtime ID", "agents.list.*.agentRuntime": "Legacy Agent Runtime", "agents.list.*.agentRuntime.id": "Legacy Agent Runtime ID", - "agents.list.*.embeddedHarness": "Agent Legacy Embedded Harness", - "agents.list.*.embeddedHarness.runtime": "Agent Legacy Embedded Harness Runtime", gateway: "Gateway", "gateway.port": "Gateway Port", "gateway.mode": "Gateway Mode", @@ -711,13 +705,14 @@ export const FIELD_LABELS: Record = { "agents.list[].runRetries.perProfile": "Agent Run Retries Per Profile", "agents.list[].runRetries.min": "Agent Run Retries Minimum", "agents.list[].runRetries.max": "Agent Run Retries Maximum", - "agents.defaults.embeddedPi": "Embedded Pi", - "agents.defaults.embeddedPi.projectSettingsPolicy": "Embedded Pi Project Settings Policy", - "agents.defaults.embeddedPi.executionContract": "Embedded Pi Execution Contract", + "agents.defaults.embeddedAgent": "Embedded OpenClaw", + "agents.defaults.embeddedAgent.projectSettingsPolicy": + "Embedded OpenClaw Project Settings Policy", + "agents.defaults.embeddedAgent.executionContract": "Embedded OpenClaw Execution Contract", "agents.defaults.heartbeat.includeSystemPromptSection": "Heartbeat Include System Prompt Section", "agents.list.*.heartbeat.includeSystemPromptSection": "Heartbeat Include System Prompt Section", - "agents.list[].embeddedPi": "Agent Embedded Pi", - "agents.list[].embeddedPi.executionContract": "Agent Embedded Pi Execution Contract", + "agents.list[].embeddedAgent": "Agent Embedded OpenClaw", + "agents.list[].embeddedAgent.executionContract": "Agent Embedded OpenClaw Execution Contract", "agents.defaults.heartbeat.directPolicy": "Heartbeat Direct Policy", "agents.list.*.heartbeat.directPolicy": "Heartbeat Direct Policy", "agents.defaults.heartbeat.suppressToolErrorWarnings": "Heartbeat Suppress Tool Error Warnings", diff --git a/src/config/sessions/store-load.ts b/src/config/sessions/store-load.ts index a8c66c22759..ddb497d00aa 100644 --- a/src/config/sessions/store-load.ts +++ b/src/config/sessions/store-load.ts @@ -309,7 +309,7 @@ function normalizeSessionEntryDelivery(entry: SessionEntry): SessionEntry { // resolvedSkills carries the full parsed Skill[] (including each SKILL.md body) // and is only used as an in-turn cache by the runtime — see -// src/agents/pi-embedded-runner/skills-runtime.ts. Persisting it bloats +// src/agents/embedded-agent-runner/skills-runtime.ts. Persisting it bloats // sessions.json by orders of magnitude when many sessions are active. Strip // it from every entry that flows through normalize, so neither the in-memory // store reloaded from disk nor the JSON serialized back to disk carries it. diff --git a/src/config/sessions/store.skills-stripping.test.ts b/src/config/sessions/store.skills-stripping.test.ts index bb31e858929..e98eef0f3e4 100644 --- a/src/config/sessions/store.skills-stripping.test.ts +++ b/src/config/sessions/store.skills-stripping.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { resolveEmbeddedRunSkillEntries } from "../../agents/pi-embedded-runner/skills-runtime.js"; +import { resolveEmbeddedRunSkillEntries } from "../../agents/embedded-agent-runner/skills-runtime.js"; import { createCanonicalFixtureSkill } from "../../agents/skills.test-helpers.js"; import type { Skill } from "../../agents/skills/skill-contract.js"; import { diff --git a/src/config/sessions/transcript-append.ts b/src/config/sessions/transcript-append.ts index 7751e4822d0..e9ccad0fd96 100644 --- a/src/config/sessions/transcript-append.ts +++ b/src/config/sessions/transcript-append.ts @@ -2,29 +2,24 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { StringDecoder } from "node:string_decoder"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../../agents/runtime/index.js"; import { acquireSessionWriteLock, resolveSessionWriteLockOptions, } from "../../agents/session-write-lock.js"; +import { CURRENT_SESSION_VERSION } from "../../agents/sessions/index.js"; import { redactTranscriptMessage } from "../../agents/transcript-redact.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { redactSecrets } from "../../logging/redact.js"; +import { createSessionTranscriptHeader } from "./transcript-header.js"; import { streamSessionTranscriptLinesReverse } from "./transcript-stream.js"; import { resolveOwnedSessionTranscriptWriteLockRunner } from "./transcript-write-context.js"; const TRANSCRIPT_APPEND_SCAN_CHUNK_BYTES = 64 * 1024; const SESSION_MANAGER_APPEND_MAX_BYTES = 8 * 1024 * 1024; -let piCodingAgentModulePromise: Promise | null = - null; const transcriptAppendQueues = new Map>(); -async function loadCurrentSessionVersion(): Promise { - piCodingAgentModulePromise ??= import("@earendil-works/pi-coding-agent"); - return (await piCodingAgentModulePromise).CURRENT_SESSION_VERSION; -} - type TranscriptLeafInfo = { leafId?: string; hasParentLinkedEntries: boolean; @@ -130,7 +125,6 @@ async function migrateLinearTranscriptToParentLinked(transcriptPath: string): Pr leafId?: string; }> { const raw = await fs.readFile(transcriptPath, "utf-8"); - const currentSessionVersion = await loadCurrentSessionVersion(); const existingIds = new Set(); const output: string[] = []; let previousId: string | null = null; @@ -152,7 +146,7 @@ async function migrateLinearTranscriptToParentLinked(transcriptPath: string): Pr } const record = parsed as Record; if (record.type === "session") { - output.push(JSON.stringify({ ...record, version: currentSessionVersion })); + output.push(JSON.stringify({ ...record, version: CURRENT_SESSION_VERSION })); continue; } const id = normalizeEntryId(record.id) ?? generateEntryId(existingIds); @@ -184,15 +178,8 @@ async function ensureTranscriptHeader( if (stat?.isFile() && stat.size > 0) { return; } - const currentSessionVersion = await loadCurrentSessionVersion(); await fs.mkdir(path.dirname(transcriptPath), { recursive: true }); - const header = { - type: "session", - version: currentSessionVersion, - id: params.sessionId ?? randomUUID(), - timestamp: new Date().toISOString(), - cwd: params.cwd ?? process.cwd(), - }; + const header = createSessionTranscriptHeader(params); await fs.writeFile(transcriptPath, `${JSON.stringify(header)}\n`, { encoding: "utf-8", mode: 0o600, diff --git a/src/config/sessions/transcript-header.ts b/src/config/sessions/transcript-header.ts new file mode 100644 index 00000000000..e073f4b945e --- /dev/null +++ b/src/config/sessions/transcript-header.ts @@ -0,0 +1,17 @@ +import { randomUUID } from "node:crypto"; +import { CURRENT_SESSION_VERSION } from "../../agents/sessions/index.js"; + +export type SessionTranscriptHeaderParams = { + sessionId?: string; + cwd?: string; +}; + +export function createSessionTranscriptHeader(params: SessionTranscriptHeaderParams = {}) { + return { + type: "session", + version: CURRENT_SESSION_VERSION, + id: params.sessionId ?? randomUUID(), + timestamp: new Date().toISOString(), + cwd: params.cwd ?? process.cwd(), + }; +} diff --git a/src/config/sessions/transcript.ts b/src/config/sessions/transcript.ts index 7e60e481391..d0965f1ff21 100644 --- a/src/config/sessions/transcript.ts +++ b/src/config/sessions/transcript.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AgentMessage } from "../../agents/runtime/index.js"; +import type { SessionManager } from "../../agents/sessions/index.js"; import { redactTranscriptMessage } from "../../agents/transcript-redact.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { emitSessionTranscriptUpdate } from "../../sessions/transcript-events.js"; @@ -17,6 +17,7 @@ import { resolveAndPersistSessionFile } from "./session-file.js"; import { loadSessionStore, resolveSessionStoreEntry } from "./store.js"; import { parseSessionThreadInfo } from "./thread-info.js"; import { appendSessionTranscriptMessage } from "./transcript-append.js"; +import { createSessionTranscriptHeader } from "./transcript-header.js"; import { resolveMirroredTranscriptText } from "./transcript-mirror.js"; import { streamSessionTranscriptLinesReverse } from "./transcript-stream.js"; import { @@ -25,16 +26,6 @@ import { } from "./transcript-write-context.js"; import type { SessionEntry } from "./types.js"; -let piCodingAgentModulePromise: Promise | null = - null; - -async function loadPiCodingAgentModule(): Promise< - typeof import("@earendil-works/pi-coding-agent") -> { - piCodingAgentModulePromise ??= import("@earendil-works/pi-coding-agent"); - return await piCodingAgentModulePromise; -} - async function ensureSessionHeader(params: { sessionFile: string; sessionId: string; @@ -42,15 +33,8 @@ async function ensureSessionHeader(params: { if (fs.existsSync(params.sessionFile)) { return; } - const { CURRENT_SESSION_VERSION } = await loadPiCodingAgentModule(); await fs.promises.mkdir(path.dirname(params.sessionFile), { recursive: true }); - const header = { - type: "session", - version: CURRENT_SESSION_VERSION, - id: params.sessionId, - timestamp: new Date().toISOString(), - cwd: process.cwd(), - }; + const header = createSessionTranscriptHeader({ sessionId: params.sessionId }); await fs.promises.writeFile(params.sessionFile, `${JSON.stringify(header)}\n`, { encoding: "utf-8", mode: 0o600, diff --git a/src/config/sessions/types.ts b/src/config/sessions/types.ts index 53b93121f90..7065a345b6a 100644 --- a/src/config/sessions/types.ts +++ b/src/config/sessions/types.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -import type { Skill } from "@earendil-works/pi-coding-agent"; +import type { Skill } from "../../agents/sessions/index.js"; import type { ChatType } from "../../channels/chat-type.js"; import type { ChannelId } from "../../channels/plugins/channel-id.types.js"; import type { ChannelRouteRef } from "../../plugin-sdk/channel-route.js"; @@ -610,7 +610,7 @@ export type SessionSkillSnapshot = { * each SKILL.md body) so the embedded runner can skip a workspace skill * scan within a turn. Stripped from sessions.json on every read and write * via normalizeSessionStore — see store-load.ts. On a cold session resume - * this is undefined and src/agents/pi-embedded-runner/skills-runtime.ts + * this is undefined and src/agents/embedded-agent-runner/skills-runtime.ts * rebuilds it by reloading skill entries from disk. */ resolvedSkills?: Skill[]; diff --git a/src/config/types.agent-defaults.ts b/src/config/types.agent-defaults.ts index f30d32371b2..d00688a594a 100644 --- a/src/config/types.agent-defaults.ts +++ b/src/config/types.agent-defaults.ts @@ -1,6 +1,5 @@ import type { SilentReplyPolicyShape } from "../shared/silent-reply-policy.js"; import type { - AgentEmbeddedHarnessConfig, AgentModelConfig, AgentToolModelConfig, AgentRuntimePolicyConfig, @@ -16,7 +15,7 @@ import type { MemorySearchConfig } from "./types.tools.js"; export type AgentContextInjection = "always" | "continuation-skip" | "never"; export type OptionalBootstrapFileName = "SOUL.md" | "USER.md" | "HEARTBEAT.md" | "IDENTITY.md"; -export type EmbeddedPiExecutionContract = "default" | "strict-agentic"; +export type EmbeddedAgentExecutionContract = "default" | "strict-agentic"; export type SubagentDelegationMode = "suggest" | "prefer"; export type AgentImageQualityPreference = "auto" | "efficient" | "balanced" | "high"; @@ -202,12 +201,13 @@ export type CliBackendConfig = { export type AgentDefaultsConfig = { /** Global default provider params applied to all models before per-model and per-agent overrides. */ params?: Record; - /** Default agent runtime policy. */ - agentRuntime?: AgentRuntimePolicyConfig; - /** @deprecated Use agentRuntime. */ - embeddedHarness?: AgentEmbeddedHarnessConfig; /** Primary model and fallbacks (provider/model). Accepts string or {primary,fallbacks}. */ model?: AgentModelConfig; + /** + * @deprecated Legacy raw config accepted only by doctor/migration repair. + * Normal schema parsing rejects this key; use per-model agentRuntime instead. + */ + agentRuntime?: AgentRuntimePolicyConfig; /** Optional image-capable model and fallbacks (provider/model). Accepts string or {primary,fallbacks}. */ imageModel?: AgentToolModelConfig; /** Optional image-generation model and fallbacks (provider/model). Accepts string or {primary,fallbacks}. */ @@ -309,21 +309,21 @@ export type AgentDefaultsConfig = { compaction?: AgentCompactionConfig; /** Outer run loop retry iteration boundaries. */ runRetries?: AgentRunRetriesConfig; - /** Embedded Pi runner hardening and compatibility controls. */ - embeddedPi?: { + /** Embedded OpenClaw runner hardening and compatibility controls. */ + embeddedAgent?: { /** - * How embedded Pi should trust workspace-local `.pi/config/settings.json`. + * How embedded OpenClaw should trust workspace-local `.openclaw/settings.json`. * - sanitize (default): apply project settings except shellPath/shellCommandPrefix * - ignore: ignore project settings entirely * - trusted: trust project settings as-is */ projectSettingsPolicy?: "trusted" | "sanitize" | "ignore"; /** - * Embedded Pi execution contract: + * Embedded OpenClaw execution contract: * - default: keep the standard runner behavior * - strict-agentic: on OpenAI/OpenAI Codex GPT-5-family runs, keep acting until hitting a real blocker */ - executionContract?: EmbeddedPiExecutionContract; + executionContract?: EmbeddedAgentExecutionContract; }; /** Vector memory search configuration (per-agent overrides supported). */ memorySearch?: MemorySearchConfig; @@ -478,7 +478,7 @@ export type AgentCompactionQualityGuardConfig = { export type AgentCompactionMidTurnPrecheckConfig = { /** * Enable structured context pressure checks after tool results are appended - * and before the next Pi model call. Default: false. + * and before the next agent model call. Default: false. */ enabled?: boolean; }; @@ -486,11 +486,11 @@ export type AgentCompactionMidTurnPrecheckConfig = { export type AgentCompactionConfig = { /** Compaction summarization mode. */ mode?: AgentCompactionMode; - /** Pi reserve tokens target before floor enforcement. */ + /** Embedded OpenClaw reserve tokens target before floor enforcement. */ reserveTokens?: number; - /** Pi keepRecentTokens budget used for cut-point selection. */ + /** Embedded OpenClaw keepRecentTokens budget used for cut-point selection. */ keepRecentTokens?: number; - /** Minimum reserve tokens enforced for Pi compaction (0 disables the floor). */ + /** Minimum reserve tokens enforced for embedded OpenClaw compaction (0 disables the floor). */ reserveTokensFloor?: number; /** Max share of context window for history during safeguard pruning (0.1–0.9, default 0.5). */ maxHistoryShare?: number; diff --git a/src/config/types.agents-shared.ts b/src/config/types.agents-shared.ts index 1cab5c81153..b9cee3d6ea0 100644 --- a/src/config/types.agents-shared.ts +++ b/src/config/types.agents-shared.ts @@ -26,12 +26,12 @@ export type AgentToolModelConfig = }; export type AgentEmbeddedHarnessConfig = { - /** Agent runtime id. Omitted uses "pi"; "auto" opts into plugin harness auto-selection. */ + /** Agent runtime id. Omitted uses "openclaw"; "auto" opts into plugin harness auto-selection. */ runtime?: string; }; export type AgentRuntimePolicyConfig = { - /** Agent runtime id. Omitted uses "pi"; "auto" opts into plugin harness auto-selection. */ + /** Agent runtime id. Omitted uses "openclaw"; "auto" opts into plugin harness auto-selection. */ id?: string; }; diff --git a/src/config/types.agents.ts b/src/config/types.agents.ts index b3d6dedacc8..abca95fd23c 100644 --- a/src/config/types.agents.ts +++ b/src/config/types.agents.ts @@ -3,15 +3,10 @@ import type { AgentContextLimitsConfig, AgentDefaultsConfig, AgentModelEntryConfig, - EmbeddedPiExecutionContract, + EmbeddedAgentExecutionContract, SubagentDelegationMode, } from "./types.agent-defaults.js"; -import type { - AgentEmbeddedHarnessConfig, - AgentModelConfig, - AgentRuntimePolicyConfig, - AgentSandboxConfig, -} from "./types.agents-shared.js"; +import type { AgentModelConfig, AgentSandboxConfig } from "./types.agents-shared.js"; import type { DmScope, HumanDelayConfig, IdentityConfig } from "./types.base.js"; import type { GroupChatConfig } from "./types.messages.js"; import type { SkillsLimitsConfig } from "./types.skills.js"; @@ -91,11 +86,12 @@ export type AgentConfig = { agentDir?: string; /** Optional per-agent full system prompt replacement. */ systemPromptOverride?: AgentDefaultsConfig["systemPromptOverride"]; - /** Optional per-agent agent runtime policy override. */ - agentRuntime?: AgentRuntimePolicyConfig; - /** @deprecated Use agentRuntime. */ - embeddedHarness?: AgentEmbeddedHarnessConfig; model?: AgentModelConfig; + /** + * @deprecated Legacy raw config accepted only by doctor/migration repair. + * Normal schema parsing rejects this key; use per-model agentRuntime instead. + */ + agentRuntime?: AgentModelEntryConfig["agentRuntime"]; /** Per-model metadata overrides for this agent. */ models?: Record; /** @deprecated Legacy per-agent compaction config is kept for raw doctor migration/repair. */ @@ -146,10 +142,10 @@ export type AgentConfig = { }; /** Optional outer run loop retry boundaries. */ runRetries?: AgentDefaultsConfig["runRetries"]; - /** Optional per-agent embedded Pi overrides. */ - embeddedPi?: { + /** Optional per-agent embedded OpenClaw overrides. */ + embeddedAgent?: { /** Optional per-agent execution contract override. */ - executionContract?: EmbeddedPiExecutionContract; + executionContract?: EmbeddedAgentExecutionContract; }; /** Optional per-agent sandbox overrides. */ sandbox?: AgentSandboxConfig; diff --git a/src/config/types.models.ts b/src/config/types.models.ts index a9ce8b27860..b2e8669bbd7 100644 --- a/src/config/types.models.ts +++ b/src/config/types.models.ts @@ -2,7 +2,7 @@ import type { AnthropicMessagesCompat, OpenAICompletionsCompat, OpenAIResponsesCompat, -} from "@earendil-works/pi-ai"; +} from "openclaw/plugin-sdk/llm"; import type { AgentRuntimePolicyConfig } from "./types.agents-shared.js"; import type { ConfiguredModelProviderRequest } from "./types.provider-request.js"; import type { SecretInput } from "./types.secrets.js"; diff --git a/src/config/zod-schema.agent-defaults.test.ts b/src/config/zod-schema.agent-defaults.test.ts index e0bcf1881d9..0e6ebfa1dad 100644 --- a/src/config/zod-schema.agent-defaults.test.ts +++ b/src/config/zod-schema.agent-defaults.test.ts @@ -238,13 +238,37 @@ describe("agent defaults schema", () => { ); }); - it("accepts embeddedPi.executionContract", () => { + it("accepts embeddedAgent.executionContract", () => { const result = AgentDefaultsSchema.parse({ - embeddedPi: { + embeddedAgent: { executionContract: "strict-agentic", }, })!; - expect(result.embeddedPi?.executionContract).toBe("strict-agentic"); + expect(result.embeddedAgent?.executionContract).toBe("strict-agentic"); + }); + + it("rejects legacy whole-agent runtime pins outside doctor migration", () => { + expect(AgentDefaultsSchema.safeParse({ agentRuntime: { id: "codex" } }).success).toBe(false); + expect(AgentDefaultsSchema.safeParse({ embeddedHarness: { runtime: "codex" } }).success).toBe( + false, + ); + expect( + AgentEntrySchema.safeParse({ id: "legacy", agentRuntime: { id: "codex" } }).success, + ).toBe(false); + expect( + AgentEntrySchema.safeParse({ id: "legacy", embeddedHarness: { runtime: "codex" } }).success, + ).toBe(false); + }); + + it("accepts embeddedAgent project settings policy", () => { + const result = AgentDefaultsSchema.parse({ + embeddedAgent: { + executionContract: "strict-agentic", + projectSettingsPolicy: "sanitize", + }, + })!; + expect(result.embeddedAgent?.executionContract).toBe("strict-agentic"); + expect(result.embeddedAgent?.projectSettingsPolicy).toBe("sanitize"); }); it("accepts runRetries configuration on defaults and agent entries", () => { diff --git a/src/config/zod-schema.agent-defaults.ts b/src/config/zod-schema.agent-defaults.ts index 540f863fe58..da57da48006 100644 --- a/src/config/zod-schema.agent-defaults.ts +++ b/src/config/zod-schema.agent-defaults.ts @@ -4,8 +4,7 @@ import { HeartbeatSchema, AgentSandboxSchema, AgentContextLimitsSchema, - AgentEmbeddedHarnessSchema, - AgentRuntimePolicySchema, + AgentModelRuntimeEntrySchema, AgentModelSchema, AgentToolModelSchema, MemorySearchSchema, @@ -33,6 +32,15 @@ const OptionalBootstrapFileNameSchema = z.enum([ "IDENTITY.md", ]); +const EmbeddedAgentConfigSchema = z + .object({ + projectSettingsPolicy: z + .union([z.literal("trusted"), z.literal("sanitize"), z.literal("ignore")]) + .optional(), + executionContract: z.union([z.literal("default"), z.literal("strict-agentic")]).optional(), + }) + .strict(); + export const SilentReplyPolicyConfigSchema = z .object({ group: SilentReplyPolicySchema.optional(), @@ -44,8 +52,6 @@ export const AgentDefaultsSchema = z .object({ /** Global default provider params applied to all models before per-model and per-agent overrides. */ params: z.record(z.string(), z.unknown()).optional(), - agentRuntime: AgentRuntimePolicySchema, - embeddedHarness: AgentEmbeddedHarnessSchema, model: AgentModelSchema.optional(), imageModel: AgentToolModelSchema.optional(), imageGenerationModel: AgentToolModelSchema.optional(), @@ -55,21 +61,7 @@ export const AgentDefaultsSchema = z pdfModel: AgentToolModelSchema.optional(), pdfMaxBytesMb: z.number().positive().optional(), pdfMaxPages: z.number().int().positive().optional(), - models: z - .record( - z.string(), - z - .object({ - alias: z.string().optional(), - /** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */ - params: z.record(z.string(), z.unknown()).optional(), - agentRuntime: AgentRuntimePolicySchema, - /** Enable streaming for this model (default: true, false for Ollama to avoid SDK issue #1205). */ - streaming: z.boolean().optional(), - }) - .strict(), - ) - .optional(), + models: z.record(z.string(), AgentModelRuntimeEntrySchema).optional(), workspace: z.string().optional(), skills: z.array(z.string()).optional(), silentReply: SilentReplyPolicyConfigSchema.optional(), @@ -211,15 +203,7 @@ export const AgentDefaultsSchema = z .strict() .optional(), runRetries: AgentRunRetriesConfigSchema.optional(), - embeddedPi: z - .object({ - projectSettingsPolicy: z - .union([z.literal("trusted"), z.literal("sanitize"), z.literal("ignore")]) - .optional(), - executionContract: z.union([z.literal("default"), z.literal("strict-agentic")]).optional(), - }) - .strict() - .optional(), + embeddedAgent: EmbeddedAgentConfigSchema.optional(), thinkingDefault: z .union([ z.literal("off"), diff --git a/src/config/zod-schema.agent-runtime.ts b/src/config/zod-schema.agent-runtime.ts index a2dd40eecf5..5de6f0ab460 100644 --- a/src/config/zod-schema.agent-runtime.ts +++ b/src/config/zod-schema.agent-runtime.ts @@ -73,6 +73,12 @@ export const AgentRunRetriesConfigSchema = z { message: "max must be greater than or equal to min", path: ["max"] }, ); +const AgentEntryEmbeddedAgentConfigSchema = z + .object({ + executionContract: z.union([z.literal("default"), z.literal("strict-agentic")]).optional(), + }) + .strict(); + export const HeartbeatSchema = z .object({ every: z.string().optional(), @@ -995,6 +1001,15 @@ export const AgentRuntimePolicySchema = z .strict() .optional(); +export const AgentModelRuntimeEntrySchema = z + .object({ + alias: z.string().optional(), + params: z.record(z.string(), z.unknown()).optional(), + agentRuntime: AgentRuntimePolicySchema, + streaming: z.boolean().optional(), + }) + .strict(); + export const AgentEntrySchema = z .object({ id: z.string(), @@ -1004,22 +1019,8 @@ export const AgentEntrySchema = z workspace: z.string().optional(), agentDir: z.string().optional(), systemPromptOverride: z.string().optional(), - agentRuntime: AgentRuntimePolicySchema, - embeddedHarness: AgentEmbeddedHarnessSchema, model: AgentModelSchema.optional(), - models: z - .record( - z.string(), - z - .object({ - alias: z.string().optional(), - params: z.record(z.string(), z.unknown()).optional(), - agentRuntime: AgentRuntimePolicySchema, - streaming: z.boolean().optional(), - }) - .strict(), - ) - .optional(), + models: z.record(z.string(), AgentModelRuntimeEntrySchema).optional(), thinkingDefault: z .enum(["off", "minimal", "low", "medium", "high", "xhigh", "adaptive", "max"]) .optional(), @@ -1059,12 +1060,7 @@ export const AgentEntrySchema = z .strict() .optional(), runRetries: AgentRunRetriesConfigSchema.optional(), - embeddedPi: z - .object({ - executionContract: z.union([z.literal("default"), z.literal("strict-agentic")]).optional(), - }) - .strict() - .optional(), + embeddedAgent: AgentEntryEmbeddedAgentConfigSchema.optional(), sandbox: AgentSandboxSchema, params: z.record(z.string(), z.unknown()).optional(), tools: AgentToolsSchema, diff --git a/src/context-engine/context-engine.test.ts b/src/context-engine/context-engine.test.ts index 9f99454a86d..6db7a25d540 100644 --- a/src/context-engine/context-engine.test.ts +++ b/src/context-engine/context-engine.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { MemoryCitationsMode } from "../config/types.memory.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; @@ -32,16 +32,16 @@ import type { IngestResult, } from "./types.js"; -const { compactEmbeddedPiSessionDirectMock } = vi.hoisted(() => ({ - compactEmbeddedPiSessionDirectMock: vi.fn(), +const { compactEmbeddedAgentSessionDirectMock } = vi.hoisted(() => ({ + compactEmbeddedAgentSessionDirectMock: vi.fn(), })); -vi.mock("../agents/pi-embedded-runner/compact.runtime.js", () => ({ - compactEmbeddedPiSessionDirect: compactEmbeddedPiSessionDirectMock, +vi.mock("../agents/embedded-agent-runner/compact.runtime.js", () => ({ + compactEmbeddedAgentSessionDirect: compactEmbeddedAgentSessionDirectMock, })); function installCompactRuntimeSpy() { - return compactEmbeddedPiSessionDirectMock.mockResolvedValue({ + return compactEmbeddedAgentSessionDirectMock.mockResolvedValue({ ok: true, compacted: false, reason: "mock compaction", @@ -56,7 +56,7 @@ function installCompactRuntimeSpy() { } function requireCompactRuntimeParams(callIndex: number): Record { - const params = compactEmbeddedPiSessionDirectMock.mock.calls[callIndex]?.[0] as + const params = compactEmbeddedAgentSessionDirectMock.mock.calls[callIndex]?.[0] as | Record | undefined; if (!params) { @@ -379,7 +379,7 @@ class LegacyAssembleStrictEngine implements ContextEngine { describe("Engine contract tests", () => { beforeEach(() => { vi.restoreAllMocks(); - compactEmbeddedPiSessionDirectMock.mockReset(); + compactEmbeddedAgentSessionDirectMock.mockReset(); clearMemoryPluginState(); }); diff --git a/src/context-engine/delegate.ts b/src/context-engine/delegate.ts index c02a65effc6..66514b78329 100644 --- a/src/context-engine/delegate.ts +++ b/src/context-engine/delegate.ts @@ -1,11 +1,11 @@ -import type { CompactEmbeddedPiSessionDirect } from "../agents/pi-embedded-runner/compact.runtime.types.js"; +import type { CompactEmbeddedAgentSessionDirect } from "../agents/embedded-agent-runner/compact.runtime.types.js"; import { normalizeStructuredPromptSection } from "../agents/prompt-cache-stability.js"; import type { MemoryCitationsMode } from "../config/types.memory.js"; import { buildMemoryPromptSection } from "../plugins/memory-state.js"; import type { ContextEngine, CompactResult, ContextEngineRuntimeContext } from "./types.js"; type CompactRuntimeModule = { - compactEmbeddedPiSessionDirect: CompactEmbeddedPiSessionDirect; + compactEmbeddedAgentSessionDirect: CompactEmbeddedAgentSessionDirect; }; let compactRuntimePromise: Promise | null = null; @@ -13,7 +13,7 @@ let compactRuntimePromise: Promise | null = null; function loadCompactRuntime(): Promise { // Use a literal specifier so the bundler rewrites the runtime chunk path // instead of resolving a source-tree path at runtime. - compactRuntimePromise ??= import("../agents/pi-embedded-runner/compact.runtime.js"); + compactRuntimePromise ??= import("../agents/embedded-agent-runner/compact.runtime.js"); return compactRuntimePromise; } @@ -35,10 +35,10 @@ export async function delegateCompactionToRuntime( ): Promise { // Load through the dedicated runtime boundary without introducing another // source-level static edge into the embedded runner graph. - const { compactEmbeddedPiSessionDirect } = await loadCompactRuntime(); - type RuntimeCompactionParams = Parameters[0]; + const { compactEmbeddedAgentSessionDirect } = await loadCompactRuntime(); + type RuntimeCompactionParams = Parameters[0]; - // runtimeContext carries the full CompactEmbeddedPiSessionParams fields set + // runtimeContext carries the full CompactEmbeddedAgentSessionParams fields set // by runtime callers. We spread them and override the fields that come from // the public ContextEngine compact() signature directly. const runtimeContext = (params.runtimeContext ?? {}) as ContextEngineRuntimeContext & @@ -51,7 +51,7 @@ export async function delegateCompactionToRuntime( ? Math.floor(runtimeContext.currentTokenCount) : undefined); - const result = await compactEmbeddedPiSessionDirect({ + const result = await compactEmbeddedAgentSessionDirect({ ...runtimeContext, sessionId: params.sessionId, sessionFile: params.sessionFile, diff --git a/src/context-engine/host-compat.test.ts b/src/context-engine/host-compat.test.ts index e836a9b064e..abdb517732b 100644 --- a/src/context-engine/host-compat.test.ts +++ b/src/context-engine/host-compat.test.ts @@ -4,7 +4,7 @@ import { buildGenericCliContextEngineHostSupport, CODEX_APP_SERVER_CONTEXT_ENGINE_HOST, evaluateContextEngineHostSupport, - PI_EMBEDDED_CONTEXT_ENGINE_HOST, + OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST, } from "./host-compat.js"; import type { ContextEngine, ContextEngineHostCapability } from "./types.js"; @@ -17,7 +17,7 @@ function createEngine(requiredCapabilities: ContextEngineHostCapability[]): Cont "agent-run": { requiredCapabilities, unsupportedMessage: - "Use the native Codex or Pi embedded runtime, or switch contextEngine to legacy.", + "Use the native Codex or OpenClaw embedded runtime, or switch contextEngine to legacy.", }, }, }, @@ -67,7 +67,7 @@ describe("context engine host compatibility", () => { }); }); - it("allows native Codex and Pi embedded hosts to satisfy pre-prompt assembly", () => { + it("allows native Codex and OpenClaw embedded hosts to satisfy pre-prompt assembly", () => { const engine = createEngine(["assemble-before-prompt"]); assertContextEngineHostSupport({ @@ -78,7 +78,7 @@ describe("context engine host compatibility", () => { assertContextEngineHostSupport({ contextEngine: engine, operation: "agent-run", - host: PI_EMBEDDED_CONTEXT_ENGINE_HOST, + host: OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST, }); }); diff --git a/src/context-engine/host-compat.ts b/src/context-engine/host-compat.ts index 2ec835c33bc..ceaa466a546 100644 --- a/src/context-engine/host-compat.ts +++ b/src/context-engine/host-compat.ts @@ -18,9 +18,9 @@ export const GENERIC_CLI_CONTEXT_ENGINE_HOST_CAPABILITIES = [ "maintain", ] as const satisfies readonly ContextEngineHostCapability[]; -export const PI_EMBEDDED_CONTEXT_ENGINE_HOST = { - id: "pi-embedded", - label: "Pi embedded runner", +export const OPENCLAW_EMBEDDED_CONTEXT_ENGINE_HOST = { + id: "openclaw-embedded", + label: "OpenClaw embedded runner", capabilities: [ "bootstrap", "assemble-before-prompt", diff --git a/src/context-engine/legacy.ts b/src/context-engine/legacy.ts index 39cdfa9b95f..1269228484f 100644 --- a/src/context-engine/legacy.ts +++ b/src/context-engine/legacy.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../agents/runtime/index.js"; import type { MemoryCitationsMode } from "../config/types.memory.js"; import { delegateCompactionToRuntime } from "./delegate.js"; import type { @@ -16,7 +16,7 @@ import type { * * - ingest: no-op (SessionManager handles message persistence) * - assemble: pass-through (existing sanitize/validate/limit pipeline in attempt.ts handles this) - * - compact: delegates to compactEmbeddedPiSessionDirect + * - compact: delegates to compactEmbeddedAgentSessionDirect */ export class LegacyContextEngine implements ContextEngine { readonly info: ContextEngineInfo = { diff --git a/src/context-engine/types.ts b/src/context-engine/types.ts index 28896cd5890..92076ffcbf0 100644 --- a/src/context-engine/types.ts +++ b/src/context-engine/types.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../agents/runtime/index.js"; import type { MemoryCitationsMode } from "../config/types.memory.js"; // Result types @@ -247,7 +247,7 @@ export interface ContextEngine { * Run transcript maintenance after bootstrap, successful turns, or compaction. * * Engines can use runtimeContext.rewriteTranscriptEntries() to request safe - * branch-and-reappend transcript rewrites without depending on Pi internals. + * branch-and-reappend transcript rewrites without depending on runner internals. */ maintain?(params: { sessionId: string; diff --git a/src/crestodian/assistant.test.ts b/src/crestodian/assistant.test.ts index 7faef881d5c..a08f33ecaa5 100644 --- a/src/crestodian/assistant.test.ts +++ b/src/crestodian/assistant.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, vi } from "vitest"; import type { RunCliAgentParams } from "../agents/cli-runner/types.js"; -import type { RunEmbeddedPiAgentParams } from "../agents/pi-embedded-runner/run/params.js"; -import type { EmbeddedPiRunResult } from "../agents/pi-embedded.js"; +import type { RunEmbeddedAgentParams } from "../agents/embedded-agent-runner/run/params.js"; +import type { EmbeddedAgentRunResult } from "../agents/embedded-agent.js"; import { selectCrestodianLocalPlannerBackends } from "./assistant-backends.js"; import { buildCrestodianAssistantUserPrompt, @@ -116,12 +116,12 @@ describe("Crestodian assistant", () => { it("uses Claude CLI first for configless planning", async () => { const runCliAgent = vi.fn( - async (_params: RunCliAgentParams): Promise => ({ + async (_params: RunCliAgentParams): Promise => ({ payloads: [{ text: '{"reply":"Checking the shell.","command":"status"}' }], meta: { durationMs: 0 }, }), ); - const runEmbeddedPiAgent = vi.fn(); + const runEmbeddedAgent = vi.fn(); const result = await planCrestodianCommandWithLocalRuntime({ input: "what is going on", @@ -131,7 +131,7 @@ describe("Crestodian assistant", () => { }), deps: { runCliAgent, - runEmbeddedPiAgent, + runEmbeddedAgent, createTempDir: async () => "/tmp/crestodian-planner", removeTempDir: async () => {}, }, @@ -154,7 +154,7 @@ describe("Crestodian assistant", () => { expect(firstCliDefaults.cliBackends).toBeUndefined(); expect(firstCliCall.extraSystemPrompt).toBeTypeOf("string"); expect(firstCliCall.extraSystemPrompt).toContain("Do not use tools, shell commands"); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(runEmbeddedAgent).not.toHaveBeenCalled(); }); it("selects local planner backends without execution state", () => { @@ -188,8 +188,8 @@ describe("Crestodian assistant", () => { const runCliAgent = vi.fn(async () => { throw new Error("claude unavailable"); }); - const runEmbeddedPiAgent = vi.fn( - async (_params: RunEmbeddedPiAgentParams): Promise => ({ + const runEmbeddedAgent = vi.fn( + async (_params: RunEmbeddedAgentParams): Promise => ({ meta: { durationMs: 0, finalAssistantVisibleText: '{"reply":"Codex planner online.","command":"gateway status"}', @@ -205,7 +205,7 @@ describe("Crestodian assistant", () => { }), deps: { runCliAgent, - runEmbeddedPiAgent, + runEmbeddedAgent, createTempDir: async () => "/tmp/crestodian-planner", removeTempDir: async () => {}, }, @@ -217,8 +217,8 @@ describe("Crestodian assistant", () => { expect(result.reply).toBe("Codex planner online."); expect(result.modelLabel).toBe("openai/gpt-5.5 via codex"); - expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1); - const firstEmbeddedCall = firstMockArg(runEmbeddedPiAgent); + expect(runEmbeddedAgent).toHaveBeenCalledTimes(1); + const firstEmbeddedCall = firstMockArg(runEmbeddedAgent); expect(firstEmbeddedCall.provider).toBe("openai"); expect(firstEmbeddedCall.model).toBe("gpt-5.5"); expect(firstEmbeddedCall.agentHarnessId).toBe("codex"); @@ -236,10 +236,10 @@ describe("Crestodian assistant", () => { }); it("does not fall back to Codex CLI if the app-server planner is not usable", async () => { - const runCliAgent = vi.fn(async (): Promise => { + const runCliAgent = vi.fn(async (): Promise => { throw new Error("unexpected cli provider"); }); - const runEmbeddedPiAgent = vi.fn(async () => { + const runEmbeddedAgent = vi.fn(async () => { throw new Error("codex app-server unavailable"); }); @@ -250,14 +250,14 @@ describe("Crestodian assistant", () => { }), deps: { runCliAgent, - runEmbeddedPiAgent, + runEmbeddedAgent, createTempDir: async () => "/tmp/crestodian-planner", removeTempDir: async () => {}, }, }); expect(result).toBeNull(); - expect(runEmbeddedPiAgent).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgent).toHaveBeenCalledTimes(1); expect(runCliAgent).not.toHaveBeenCalled(); }); }); diff --git a/src/crestodian/assistant.ts b/src/crestodian/assistant.ts index 5dd192a6777..72eaaf38954 100644 --- a/src/crestodian/assistant.ts +++ b/src/crestodian/assistant.ts @@ -3,7 +3,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { resolveDefaultAgentId } from "../agents/agent-scope.js"; -import { extractAssistantText } from "../agents/pi-embedded-utils.js"; +import { extractAssistantText } from "../agents/embedded-agent-utils.js"; import { completeWithPreparedSimpleCompletionModel, prepareSimpleCompletionModelForAgent, @@ -32,7 +32,7 @@ export type CrestodianAssistantPlanner = (params: { }) => Promise; type RunCliAgentFn = typeof import("../agents/cli-runner.js").runCliAgent; -type RunEmbeddedPiAgentFn = typeof import("../agents/pi-embedded.js").runEmbeddedPiAgent; +type RunEmbeddedAgentFn = typeof import("../agents/embedded-agent.js").runEmbeddedAgent; type ReadConfigFileSnapshotFn = typeof readConfigFileSnapshot; type PrepareSimpleCompletionModelForAgentFn = typeof prepareSimpleCompletionModelForAgent; type CompleteWithPreparedSimpleCompletionModelFn = typeof completeWithPreparedSimpleCompletionModel; @@ -45,7 +45,7 @@ export type CrestodianConfiguredModelPlannerDeps = { export type CrestodianLocalRuntimePlannerDeps = { runCliAgent?: RunCliAgentFn; - runEmbeddedPiAgent?: RunEmbeddedPiAgentFn; + runEmbeddedAgent?: RunEmbeddedAgentFn; createTempDir?: () => Promise; removeTempDir?: (dir: string) => Promise; }; @@ -209,7 +209,7 @@ async function runLocalRuntimePlanner( return extractPlannerResultText(result); } case "embedded": { - const runEmbedded = params.deps?.runEmbeddedPiAgent ?? (await loadRunEmbeddedPiAgent()); + const runEmbedded = params.deps?.runEmbeddedAgent ?? (await loadRunEmbeddedAgent()); const result = await runEmbedded({ sessionId, sessionKey, @@ -252,8 +252,8 @@ async function loadRunCliAgent(): Promise { return (await import("../agents/cli-runner.js")).runCliAgent; } -async function loadRunEmbeddedPiAgent(): Promise { - return (await import("../agents/pi-embedded.js")).runEmbeddedPiAgent; +async function loadRunEmbeddedAgent(): Promise { + return (await import("../agents/embedded-agent.js")).runEmbeddedAgent; } function extractPlannerResultText(result: { diff --git a/src/cron/isolated-agent.auth-profile-propagation.test.ts b/src/cron/isolated-agent.auth-profile-propagation.test.ts index cca4bb3b0f4..8b05ab7fd4a 100644 --- a/src/cron/isolated-agent.auth-profile-propagation.test.ts +++ b/src/cron/isolated-agent.auth-profile-propagation.test.ts @@ -2,7 +2,7 @@ import "./isolated-agent.mocks.js"; import fs from "node:fs/promises"; import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; +import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import { createCliDeps } from "./isolated-agent.delivery.test-helpers.js"; import { runCronIsolatedAgentTurn } from "./isolated-agent.js"; import { @@ -18,17 +18,17 @@ vi.mock("../plugins/provider-runtime.js", async (importOriginal) => ({ resolveExternalAuthProfilesWithPlugins: () => [], })); -function getEmbeddedPiAgentParams(): { +function getEmbeddedAgentParams(): { authProfileId?: string; authProfileIdSource?: string; } { - const [call] = vi.mocked(runEmbeddedPiAgent).mock.calls; + const [call] = vi.mocked(runEmbeddedAgent).mock.calls; if (!call) { - throw new Error("Expected embedded PI agent call for auth profile propagation"); + throw new Error("Expected embedded OpenClaw agent call for auth profile propagation"); } const [params] = call; if (typeof params !== "object" || params === null || Array.isArray(params)) { - throw new Error("Expected embedded PI agent params to be an object"); + throw new Error("Expected embedded OpenClaw agent params to be an object"); } return params; } @@ -38,7 +38,7 @@ describe("runCronIsolatedAgentTurn auth profile propagation (#20624)", () => { setupIsolatedAgentTurnMocks({ fast: true }); }); - it("passes authProfileId to runEmbeddedPiAgent when auth profiles exist", async () => { + it("passes authProfileId to runEmbeddedAgent when auth profiles exist", async () => { await withTempCronHome(async (home) => { const storePath = await writeSessionStore(home, { lastProvider: "webchat", lastTo: "" }); @@ -65,8 +65,8 @@ describe("runCronIsolatedAgentTurn auth profile propagation (#20624)", () => { "utf-8", ); - // 3. Mock runEmbeddedPiAgent to return ok - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + // 3. Mock runEmbeddedAgent to return ok + vi.mocked(runEmbeddedAgent).mockResolvedValue({ payloads: [{ text: "done" }], meta: { durationMs: 5, @@ -97,10 +97,10 @@ describe("runCronIsolatedAgentTurn auth profile propagation (#20624)", () => { }); expect(res.status).toBe("ok"); - expect(vi.mocked(runEmbeddedPiAgent)).toHaveBeenCalledTimes(1); + expect(vi.mocked(runEmbeddedAgent)).toHaveBeenCalledTimes(1); // 5. Check that authProfileId was passed - const callArgs = getEmbeddedPiAgentParams(); + const callArgs = getEmbeddedAgentParams(); expect(callArgs.authProfileId).toBe("openrouter:default"); }); diff --git a/src/cron/isolated-agent.delivery.test-helpers.ts b/src/cron/isolated-agent.delivery.test-helpers.ts index 67047c2b288..a44c8f640e7 100644 --- a/src/cron/isolated-agent.delivery.test-helpers.ts +++ b/src/cron/isolated-agent.delivery.test-helpers.ts @@ -1,5 +1,5 @@ import { expect, vi } from "vitest"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; +import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import type { CliDeps } from "../cli/deps.js"; import { runCronIsolatedAgentTurn } from "./isolated-agent.js"; import { makeCfg, makeJob } from "./isolated-agent.test-harness.js"; @@ -20,9 +20,9 @@ export function createCliDeps(overrides: Partial = {}): CliDeps { export function mockAgentPayloads( payloads: Array>, - extra: Partial>> = {}, + extra: Partial>> = {}, ): void { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + vi.mocked(runEmbeddedAgent).mockResolvedValue({ payloads, meta: { durationMs: 5, diff --git a/src/cron/isolated-agent.hook-content-wrapping.test.ts b/src/cron/isolated-agent.hook-content-wrapping.test.ts index 53ce5a1fa4f..341b67f7917 100644 --- a/src/cron/isolated-agent.hook-content-wrapping.test.ts +++ b/src/cron/isolated-agent.hook-content-wrapping.test.ts @@ -1,7 +1,7 @@ import "./isolated-agent.mocks.js"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { DEFAULT_MESSAGE, GMAIL_MODEL, @@ -12,7 +12,7 @@ import { import * as isolatedAgentRunRuntime from "./isolated-agent/run.runtime.js"; function lastEmbeddedPrompt(): string { - const calls = vi.mocked(runEmbeddedPiAgent).mock.calls; + const calls = vi.mocked(runEmbeddedAgent).mock.calls; const call = calls[calls.length - 1]; const prompt = call?.[0]?.prompt; if (typeof prompt !== "string") { @@ -24,7 +24,7 @@ function lastEmbeddedPrompt(): string { describe("runCronIsolatedAgentTurn hook content wrapping", () => { beforeEach(() => { vi.spyOn(isolatedAgentRunRuntime, "resolveThinkingDefault").mockReturnValue("off"); - vi.mocked(runEmbeddedPiAgent).mockClear(); + vi.mocked(runEmbeddedAgent).mockClear(); vi.mocked(loadModelCatalog).mockResolvedValue([]); }); diff --git a/src/cron/isolated-agent.lane.test.ts b/src/cron/isolated-agent.lane.test.ts index 59a4e510e85..4bcba93e0d6 100644 --- a/src/cron/isolated-agent.lane.test.ts +++ b/src/cron/isolated-agent.lane.test.ts @@ -1,7 +1,7 @@ import "./isolated-agent.mocks.js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { clearAllBootstrapSnapshots } from "../agents/bootstrap-cache.js"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; +import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import { clearSessionStoreCacheForTest } from "../config/sessions/store.js"; import { resetAgentRunContextForTest } from "../infra/agent-events.js"; import { createCliDeps, mockAgentPayloads } from "./isolated-agent.delivery.test-helpers.js"; @@ -14,7 +14,7 @@ import { } from "./isolated-agent.test-harness.js"; function lastEmbeddedLane(): string | undefined { - const calls = vi.mocked(runEmbeddedPiAgent).mock.calls; + const calls = vi.mocked(runEmbeddedAgent).mock.calls; expect(calls.length).toBeGreaterThan(0); return (calls.at(-1)?.[0] as { lane?: string } | undefined)?.lane; } @@ -63,14 +63,14 @@ function restoreSnapshotEnv() { describe("runCronIsolatedAgentTurn lane selection", () => { beforeEach(() => { - vi.mocked(runEmbeddedPiAgent).mockClear(); + vi.mocked(runEmbeddedAgent).mockClear(); }); afterEach(() => { // Shared-worker runs can start collecting the next file before the generic // runner cleanup resets env and session-store globals. restoreSnapshotEnv(); - vi.doUnmock("../agents/pi-embedded.js"); + vi.doUnmock("../agents/embedded-agent.js"); vi.doUnmock("../agents/model-catalog.js"); vi.doUnmock("../agents/model-selection.js"); vi.doUnmock("../agents/subagent-announce.js"); diff --git a/src/cron/isolated-agent.mocks.ts b/src/cron/isolated-agent.mocks.ts index 7b4e4b6fc18..13b3970b040 100644 --- a/src/cron/isolated-agent.mocks.ts +++ b/src/cron/isolated-agent.mocks.ts @@ -4,9 +4,9 @@ import { makeIsolatedAgentParamsFixture, } from "./isolated-agent/job-fixtures.js"; -vi.mock("../agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - runEmbeddedPiAgent: vi.fn(), +vi.mock("../agents/embedded-agent.js", () => ({ + abortEmbeddedAgentRun: vi.fn().mockReturnValue(false), + runEmbeddedAgent: vi.fn(), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, })); @@ -28,7 +28,7 @@ vi.mock("../agents/subagent-announce.js", () => ({ runSubagentAnnounceFlow: vi.fn(), })); -vi.mock("./isolated-agent/run-runtime-plugins.runtime.js", () => ({ +vi.mock("../plugins/runtime-plugins.runtime.js", () => ({ ensureRuntimePluginsLoaded: vi.fn(), })); diff --git a/src/cron/isolated-agent.model-overrides.test.ts b/src/cron/isolated-agent.model-overrides.test.ts index 7d3d889c0ae..69605a90ecd 100644 --- a/src/cron/isolated-agent.model-overrides.test.ts +++ b/src/cron/isolated-agent.model-overrides.test.ts @@ -1,8 +1,8 @@ import "./isolated-agent.mocks.js"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { BASE_THINKING_LEVELS } from "../auto-reply/thinking.shared.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import type { PluginProviderRegistration } from "../plugins/registry.js"; @@ -68,7 +68,7 @@ const OPENAI_PI_RUNTIME_CONFIG: Partial = { providers: { openai: { baseUrl: "https://api.openai.com/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, @@ -81,7 +81,7 @@ describe("runCronIsolatedAgentTurn model overrides", () => { resetPluginRuntimeStateForTest(); installThinkingTestProviders(); vi.spyOn(isolatedAgentRunRuntime, "resolveThinkingDefault").mockReturnValue("off"); - vi.mocked(runEmbeddedPiAgent).mockClear(); + vi.mocked(runEmbeddedAgent).mockClear(); vi.mocked(loadModelCatalog).mockResolvedValue([]); }); @@ -96,7 +96,7 @@ describe("runCronIsolatedAgentTurn model overrides", () => { }); expect(res.status).toBe("ok"); - expect(vi.mocked(runEmbeddedPiAgent)).toHaveBeenCalledTimes(1); + expect(vi.mocked(runEmbeddedAgent)).toHaveBeenCalledTimes(1); }); }); @@ -172,7 +172,7 @@ describe("runCronIsolatedAgentTurn model overrides", () => { }); gmailModel.assert(); - vi.mocked(runEmbeddedPiAgent).mockClear(); + vi.mocked(runEmbeddedAgent).mockClear(); res = ( await runGmailHookTurn(home, { "agent:main:hook:gmail:msg-1": { @@ -244,7 +244,7 @@ describe("runCronIsolatedAgentTurn model overrides", () => { expect(res.status).toBe("error"); expect(res.error).toMatch("cron payload.model 'openai/' rejected: invalid model"); - expect(vi.mocked(runEmbeddedPiAgent)).not.toHaveBeenCalled(); + expect(vi.mocked(runEmbeddedAgent)).not.toHaveBeenCalled(); }); }); @@ -257,7 +257,7 @@ describe("runCronIsolatedAgentTurn model overrides", () => { mockTexts: ["done"], }); - const calls = vi.mocked(runEmbeddedPiAgent).mock.calls; + const calls = vi.mocked(runEmbeddedAgent).mock.calls; const callArgs = calls[calls.length - 1]?.[0]; expect(callArgs?.thinkLevel).toBe("low"); }); @@ -289,7 +289,7 @@ describe("runCronIsolatedAgentTurn model overrides", () => { mockTexts: ["done"], }); - const calls = vi.mocked(runEmbeddedPiAgent).mock.calls; + const calls = vi.mocked(runEmbeddedAgent).mock.calls; const callArgs = calls[calls.length - 1]?.[0]; expect(callArgs?.provider).toBe("google"); expect(callArgs?.model).toBe("gemini-3-flash-preview"); diff --git a/src/cron/isolated-agent.model-preflight.test.ts b/src/cron/isolated-agent.model-preflight.test.ts index 174d6ddcde9..6171fbc00c6 100644 --- a/src/cron/isolated-agent.model-preflight.test.ts +++ b/src/cron/isolated-agent.model-preflight.test.ts @@ -6,7 +6,7 @@ import { resolveConfiguredModelRefMock, resolveCronSessionMock, resetRunCronIsolatedAgentTurnHarness, - runEmbeddedPiAgentMock, + runEmbeddedAgentMock, } from "./isolated-agent/run.test-harness.js"; const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn(); @@ -77,6 +77,6 @@ describe("runCronIsolatedAgentTurn model provider preflight", () => { expect(result.model).toBe("qwen3:32b"); expect(result.sessionId).toBe("cron-session"); expect(result.error).toContain("local provider endpoint is not reachable"); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); }); }); diff --git a/src/cron/isolated-agent.run-timeout-override.test.ts b/src/cron/isolated-agent.run-timeout-override.test.ts index dc05459ccc5..4c296eaff9e 100644 --- a/src/cron/isolated-agent.run-timeout-override.test.ts +++ b/src/cron/isolated-agent.run-timeout-override.test.ts @@ -1,7 +1,7 @@ import "./isolated-agent.mocks.js"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { clearAllBootstrapSnapshots } from "../agents/bootstrap-cache.js"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; +import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import { clearSessionStoreCacheForTest } from "../config/sessions/store.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { resetAgentRunContextForTest } from "../infra/agent-events.js"; @@ -15,7 +15,7 @@ import { } from "./isolated-agent.test-harness.js"; function lastEmbeddedCall(): { runTimeoutOverrideMs?: number; timeoutMs?: number } { - const calls = vi.mocked(runEmbeddedPiAgent).mock.calls; + const calls = vi.mocked(runEmbeddedAgent).mock.calls; expect(calls.length).toBeGreaterThan(0); return calls.at(-1)?.[0] as { runTimeoutOverrideMs?: number; timeoutMs?: number }; } @@ -31,7 +31,7 @@ function makeTimeoutTestCfg( providers: { openai: { baseUrl: "https://api.openai.com/v1", - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, models: [], }, }, @@ -60,12 +60,12 @@ function restoreSnapshotEnv() { describe("runCronIsolatedAgentTurn — explicit per-run timeout signal", () => { beforeEach(() => { - vi.mocked(runEmbeddedPiAgent).mockClear(); + vi.mocked(runEmbeddedAgent).mockClear(); }); afterEach(() => { restoreSnapshotEnv(); - vi.doUnmock("../agents/pi-embedded.js"); + vi.doUnmock("../agents/embedded-agent.js"); vi.doUnmock("../agents/model-catalog.js"); vi.doUnmock("../agents/model-selection.js"); vi.doUnmock("../agents/subagent-announce.js"); diff --git a/src/cron/isolated-agent.session-identity.test.ts b/src/cron/isolated-agent.session-identity.test.ts index c7d44c957f3..812b768b270 100644 --- a/src/cron/isolated-agent.session-identity.test.ts +++ b/src/cron/isolated-agent.session-identity.test.ts @@ -24,7 +24,7 @@ import { setupRunCronIsolatedAgentTurnSuite } from "./isolated-agent/run.suite-h import { dispatchCronDeliveryMock, mockRunCronFallbackPassthrough, - runEmbeddedPiAgentMock, + runEmbeddedAgentMock, updateSessionStoreMock, } from "./isolated-agent/run.test-harness.js"; import { normalizeCronJobCreate } from "./normalize.js"; @@ -40,14 +40,14 @@ function lastEmbeddedAgentCall(): { workspaceDir?: string; sessionFile?: string; } { - const calls = runEmbeddedPiAgentMock.mock.calls; + const calls = runEmbeddedAgentMock.mock.calls; const call = calls[calls.length - 1]; if (!call) { - throw new Error("expected runEmbeddedPiAgent call"); + throw new Error("expected runEmbeddedAgent call"); } const value = call[0]; if (!value || typeof value !== "object" || Array.isArray(value)) { - throw new Error("expected runEmbeddedPiAgent call payload"); + throw new Error("expected runEmbeddedAgent call payload"); } return value as { agentDir?: string; @@ -62,11 +62,11 @@ function lastEmbeddedAgentCall(): { describe("runCronIsolatedAgentTurn session identity", () => { beforeEach(() => { vi.spyOn(modelThinkingDefault, "resolveThinkingDefault").mockReturnValue("off"); - runEmbeddedPiAgentMock.mockClear(); + runEmbeddedAgentMock.mockClear(); mockRunCronFallbackPassthrough(); }); - it("passes resolved agentDir to runEmbeddedPiAgent", async () => { + it("passes resolved agentDir to runEmbeddedAgent", async () => { await withTempHome(async (home) => { const { res } = await runCronTurn(home, { jobPayload: DEFAULT_AGENT_TURN_PAYLOAD, @@ -167,7 +167,7 @@ describe("runCronIsolatedAgentTurn session identity", () => { systemSent: true, }, }); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "ok" }], meta: { durationMs: 5, diff --git a/src/cron/isolated-agent.test-setup.ts b/src/cron/isolated-agent.test-setup.ts index 963fd5b1adc..18892798743 100644 --- a/src/cron/isolated-agent.test-setup.ts +++ b/src/cron/isolated-agent.test-setup.ts @@ -1,6 +1,6 @@ import { vi } from "vitest"; +import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { runSubagentAnnounceFlow } from "../agents/subagent-announce.js"; import type { ChannelOutboundAdapter, @@ -153,7 +153,7 @@ export function setupIsolatedAgentTurnMocks(params?: { fast?: boolean }): void { if (params?.fast) { vi.stubEnv("OPENCLAW_TEST_FAST", "1"); } - vi.mocked(runEmbeddedPiAgent).mockReset(); + vi.mocked(runEmbeddedAgent).mockReset(); vi.mocked(loadModelCatalog).mockResolvedValue([]); vi.mocked(runSubagentAnnounceFlow).mockReset().mockResolvedValue(true); vi.mocked(callGateway).mockReset().mockResolvedValue({ ok: true, deleted: true }); diff --git a/src/cron/isolated-agent.turn-test-helpers.ts b/src/cron/isolated-agent.turn-test-helpers.ts index 7c3345c225a..d6cc05f75be 100644 --- a/src/cron/isolated-agent.turn-test-helpers.ts +++ b/src/cron/isolated-agent.turn-test-helpers.ts @@ -1,7 +1,7 @@ import "./isolated-agent.mocks.js"; import fs from "node:fs/promises"; import { expect, vi } from "vitest"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; +import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import type { CliDeps } from "../cli/deps.js"; import { runCronIsolatedAgentTurn } from "./isolated-agent.js"; import { @@ -26,7 +26,7 @@ export function makeDeps(): CliDeps { } function mockEmbeddedPayloads(payloads: Array<{ text?: string; isError?: boolean }>) { - vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + vi.mocked(runEmbeddedAgent).mockResolvedValue({ payloads, meta: { durationMs: 5, @@ -44,7 +44,7 @@ export function mockEmbeddedOk() { } export function expectEmbeddedProviderModel(expected: { provider: string; model: string }) { - const call = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0] as { + const call = vi.mocked(runEmbeddedAgent).mock.calls.at(-1)?.[0] as { provider?: string; model?: string; }; @@ -101,7 +101,7 @@ export async function runCronTurn(home: string, options: RunCronTurnOptions = {} })); const deps = options.deps ?? makeDeps(); if (options.mockTexts === null) { - vi.mocked(runEmbeddedPiAgent).mockClear(); + vi.mocked(runEmbeddedAgent).mockClear(); } else { mockEmbeddedTexts(options.mockTexts ?? ["ok"]); } diff --git a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts index 56902a8de5a..daa50ceed8c 100644 --- a/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts +++ b/src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts @@ -76,7 +76,7 @@ vi.mock("../../agents/subagent-registry-read.js", () => ({ countActiveDescendantRuns: countActiveDescendantRunsMock, })); -vi.mock("../../agents/pi-bundle-mcp-tools.js", () => ({ +vi.mock("../../agents/agent-bundle-mcp-tools.js", () => ({ retireSessionMcpRuntime: retireSessionMcpRuntimeMock, })); @@ -137,7 +137,7 @@ vi.mock("./subagent-followup.runtime.js", () => ({ waitForDescendantSubagentSummary: vi.fn().mockResolvedValue(undefined), })); -import { retireSessionMcpRuntime } from "../../agents/pi-bundle-mcp-tools.js"; +import { retireSessionMcpRuntime } from "../../agents/agent-bundle-mcp-tools.js"; // Import after mocks import { countActiveDescendantRuns } from "../../agents/subagent-registry-read.js"; import { appendAssistantMessageToSessionTranscript } from "../../config/sessions/transcript.runtime.js"; diff --git a/src/cron/isolated-agent/delivery-dispatch.ts b/src/cron/isolated-agent/delivery-dispatch.ts index 27b919c770a..2fda49d8c96 100644 --- a/src/cron/isolated-agent/delivery-dispatch.ts +++ b/src/cron/isolated-agent/delivery-dispatch.ts @@ -1,4 +1,4 @@ -import { retireSessionMcpRuntime } from "../../agents/pi-bundle-mcp-tools.js"; +import { retireSessionMcpRuntime } from "../../agents/agent-bundle-mcp-tools.js"; import type { ReplyPayload } from "../../auto-reply/reply-payload.js"; import { isSilentReplyText, diff --git a/src/cron/isolated-agent/run-embedded.runtime.ts b/src/cron/isolated-agent/run-embedded.runtime.ts index 765e19177a1..00e14d0579c 100644 --- a/src/cron/isolated-agent/run-embedded.runtime.ts +++ b/src/cron/isolated-agent/run-embedded.runtime.ts @@ -1,3 +1,3 @@ export { resolveFastModeState } from "../../agents/fast-mode.js"; export { resolveCronAgentLane } from "../../agents/lanes.js"; -export { runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; +export { runEmbeddedAgent } from "../../agents/embedded-agent.js"; diff --git a/src/cron/isolated-agent/run-executor.ts b/src/cron/isolated-agent/run-executor.ts index b8a4a80efae..8d2d3b04438 100644 --- a/src/cron/isolated-agent/run-executor.ts +++ b/src/cron/isolated-agent/run-executor.ts @@ -221,13 +221,13 @@ export function createCronPromptExecutor(params: { ); return result; } - const { resolveFastModeState, runEmbeddedPiAgent } = await loadCronEmbeddedRuntime(); + const { resolveFastModeState, runEmbeddedAgent } = await loadCronEmbeddedRuntime(); const currentChannelId = await resolveCurrentChannelTarget({ channel: messageChannel, to: params.resolvedDelivery.to, threadId: params.resolvedDelivery.threadId, }); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ sessionId: params.cronSession.sessionEntry.sessionId, sessionKey: params.runSessionKey, agentId: params.agentId, diff --git a/src/cron/isolated-agent/run-runtime-plugins.runtime.ts b/src/cron/isolated-agent/run-runtime-plugins.runtime.ts deleted file mode 100644 index 9a7872300f9..00000000000 --- a/src/cron/isolated-agent/run-runtime-plugins.runtime.ts +++ /dev/null @@ -1 +0,0 @@ -export { ensureRuntimePluginsLoaded } from "../../agents/runtime-plugins.js"; diff --git a/src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts b/src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts index d9429718333..517feea211f 100644 --- a/src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts +++ b/src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts @@ -16,7 +16,7 @@ import { resolveSupportedThinkingLevelMock, resetRunCronIsolatedAgentTurnHarness, restoreFastTestEnv, - runEmbeddedPiAgentMock, + runEmbeddedAgentMock, runWithModelFallbackMock, updateSessionStoreMock, runCliAgentMock, @@ -164,12 +164,12 @@ describe("runCronIsolatedAgentTurn — cron model override forwarding (#58065)", }); it("passes the cron payload model to the embedded agent runner", async () => { - // Use passthrough so runEmbeddedPiAgentMock actually gets called + // Use passthrough so runEmbeddedAgentMock actually gets called runWithModelFallbackMock.mockImplementation(async ({ provider, model, run }) => { const result = await run(provider, model); return { result, provider, model, attempts: [] }; }); - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "summary done" }], meta: { agentMeta: { usage: { input: 10, output: 20 } } }, }); @@ -177,7 +177,7 @@ describe("runCronIsolatedAgentTurn — cron model override forwarding (#58065)", const result = await runCronIsolatedAgentTurn(makeParams()); expect(result.status).toBe("ok"); - const embeddedCall = firstMockArg(runEmbeddedPiAgentMock); + const embeddedCall = firstMockArg(runEmbeddedAgentMock); expect(embeddedCall.provider).toBe("google"); expect(embeddedCall.model).toBe("gemini-2.0-flash"); }); @@ -187,7 +187,7 @@ describe("runCronIsolatedAgentTurn — cron model override forwarding (#58065)", const result = await run(provider, model); return { result, provider, model, attempts: [] }; }); - runEmbeddedPiAgentMock.mockImplementation(async ({ onExecutionPhase }) => { + runEmbeddedAgentMock.mockImplementation(async ({ onExecutionPhase }) => { onExecutionPhase?.({ phase: "model_call_started", provider: "google", @@ -329,7 +329,7 @@ describe("runCronIsolatedAgentTurn — cron model override forwarding (#58065)", expect(catalogEntry.id).toBe("qwen3:0.6b"); expect(catalogEntry.reasoning).toBe(true); - const embeddedCall = firstMockArg(runEmbeddedPiAgentMock); + const embeddedCall = firstMockArg(runEmbeddedAgentMock); expect(embeddedCall.provider).toBe("ollama"); expect(embeddedCall.model).toBe("qwen3:0.6b"); expect(embeddedCall.thinkLevel).toBe("medium"); diff --git a/src/cron/isolated-agent/run.fast-mode.test.ts b/src/cron/isolated-agent/run.fast-mode.test.ts index 617ef08b977..6a3c5296025 100644 --- a/src/cron/isolated-agent/run.fast-mode.test.ts +++ b/src/cron/isolated-agent/run.fast-mode.test.ts @@ -10,7 +10,7 @@ import { retireSessionMcpRuntimeMock, resolveFastModeStateMock, resolveCronSessionMock, - runEmbeddedPiAgentMock, + runEmbeddedAgentMock, runWithModelFallbackMock, } from "./run.test-harness.js"; @@ -103,8 +103,8 @@ async function runFastModeCase(params: { ); expect(result.status).toBe("ok"); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - const [embeddedRunParams] = requireFirstMockCall(runEmbeddedPiAgentMock, "embedded run"); + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); + const [embeddedRunParams] = requireFirstMockCall(runEmbeddedAgentMock, "embedded run"); expect(embeddedRunParams.provider).toBe("openai"); expect(embeddedRunParams.model).toBe(EXPECTED_OPENAI_MODEL); expect(embeddedRunParams.fastMode).toBe(params.expectedFastMode); diff --git a/src/cron/isolated-agent/run.interim-retry.test.ts b/src/cron/isolated-agent/run.interim-retry.test.ts index 22cffd3b92c..387c7da98d9 100644 --- a/src/cron/isolated-agent/run.interim-retry.test.ts +++ b/src/cron/isolated-agent/run.interim-retry.test.ts @@ -12,16 +12,16 @@ import { mockRunCronFallbackPassthrough, pickLastNonEmptyTextFromPayloadsMock, resolveCronDeliveryPlanMock, - runEmbeddedPiAgentMock, + runEmbeddedAgentMock, runWithModelFallbackMock, } from "./run.test-harness.js"; const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn(); function requireEmbeddedAgentCall(index: number): { prompt?: string } { - const call = runEmbeddedPiAgentMock.mock.calls[index]?.[0] as { prompt?: string } | undefined; + const call = runEmbeddedAgentMock.mock.calls[index]?.[0] as { prompt?: string } | undefined; if (!call) { - throw new Error(`Expected embedded PI agent call ${index}`); + throw new Error(`Expected embedded OpenClaw agent call ${index}`); } return call; } @@ -49,7 +49,7 @@ describe("runCronIsolatedAgentTurn — interim ack retry", () => { const result = await runCronIsolatedAgentTurn(makeIsolatedAgentTurnParams()); expect(result.status).toBe("ok"); expect(runWithModelFallbackMock).toHaveBeenCalledTimes(expectedFallbackCalls); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(expectedAgentCalls); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(expectedAgentCalls); return result; }; @@ -69,7 +69,7 @@ describe("runCronIsolatedAgentTurn — interim ack retry", () => { it("regression, retries once when cron returns interim acknowledgement and no descendants were spawned", async () => { usePayloadTextExtraction(); - runEmbeddedPiAgentMock + runEmbeddedAgentMock .mockResolvedValueOnce({ payloads: [ { @@ -92,7 +92,7 @@ describe("runCronIsolatedAgentTurn — interim ack retry", () => { it("does not retry when the first turn is already a concrete result", async () => { usePayloadTextExtraction(); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "SF is 62F and SD is 67F. SD is warmer by 5F." }], meta: { agentMeta: { usage: { input: 10, output: 20 } } }, }); @@ -103,7 +103,7 @@ describe("runCronIsolatedAgentTurn — interim ack retry", () => { it("does not retry over a fatal structured failure signal", async () => { usePayloadTextExtraction(); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "On it, retrying now." }], meta: { agentMeta: { usage: { input: 10, output: 20 } }, @@ -124,7 +124,7 @@ describe("runCronIsolatedAgentTurn — interim ack retry", () => { expect(result.status).toBe("error"); expect(result.error).toBe("SYSTEM_RUN_DENIED: approval required"); expect(runWithModelFallbackMock).toHaveBeenCalledTimes(1); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); }); it("delivers synthesized fatal failure signals even when the original payloads are empty", async () => { @@ -136,7 +136,7 @@ describe("runCronIsolatedAgentTurn — interim ack retry", () => { to: "123", }); isHeartbeatOnlyResponseMock.mockReturnValue(true); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [], meta: { agentMeta: { usage: { input: 10, output: 20 } }, @@ -165,7 +165,7 @@ describe("runCronIsolatedAgentTurn — interim ack retry", () => { it("does not retry when descendants were spawned in this run even if they already settled", async () => { usePayloadTextExtraction(); - runEmbeddedPiAgentMock.mockResolvedValueOnce({ + runEmbeddedAgentMock.mockResolvedValueOnce({ payloads: [{ text: "On it, I spawned a subagent and it will auto-announce when done." }], meta: { agentMeta: { usage: { input: 10, output: 20 } } }, }); diff --git a/src/cron/isolated-agent/run.live-session-model-switch.test.ts b/src/cron/isolated-agent/run.live-session-model-switch.test.ts index 2caa41e1d9a..d5dc07c2270 100644 --- a/src/cron/isolated-agent/run.live-session-model-switch.test.ts +++ b/src/cron/isolated-agent/run.live-session-model-switch.test.ts @@ -11,7 +11,7 @@ import { resolveCronSessionMock, resolveSessionAuthProfileOverrideMock, resetRunCronIsolatedAgentTurnHarness, - runEmbeddedPiAgentMock, + runEmbeddedAgentMock, runWithModelFallbackMock, updateSessionStoreMock, } from "./run.test-harness.js"; @@ -71,7 +71,7 @@ function requireEmbeddedAgentCall(index: number): { authProfileId?: string; authProfileIdSource?: string; } { - const call = runEmbeddedPiAgentMock.mock.calls[index]?.[0] as + const call = runEmbeddedAgentMock.mock.calls[index]?.[0] as | { provider?: string; model?: string; @@ -80,7 +80,7 @@ function requireEmbeddedAgentCall(index: number): { } | undefined; if (!call) { - throw new Error(`Expected embedded PI agent call ${index}`); + throw new Error(`Expected embedded OpenClaw agent call ${index}`); } return call; } @@ -201,7 +201,7 @@ describe("runCronIsolatedAgentTurn — LiveSessionModelSwitchError retry (#57206 model, attempts: [], })); - runEmbeddedPiAgentMock + runEmbeddedAgentMock .mockRejectedValueOnce( new LiveSessionModelSwitchError({ provider: "anthropic", @@ -224,7 +224,7 @@ describe("runCronIsolatedAgentTurn — LiveSessionModelSwitchError retry (#57206 const result = await runCronIsolatedAgentTurn(makeParams()); expect(result.status).toBe("ok"); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(2); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(2); const retryParams = requireEmbeddedAgentCall(1); expect(retryParams.provider).toBe("anthropic"); expect(retryParams.model).toBe("claude-sonnet-4-6"); diff --git a/src/cron/isolated-agent/run.message-tool-policy.test.ts b/src/cron/isolated-agent/run.message-tool-policy.test.ts index 4d9df708e2c..24b2413f489 100644 --- a/src/cron/isolated-agent/run.message-tool-policy.test.ts +++ b/src/cron/isolated-agent/run.message-tool-policy.test.ts @@ -18,7 +18,7 @@ import { resolveCronDeliveryPlanMock, resolveDeliveryTargetMock, restoreFastTestEnv, - runEmbeddedPiAgentMock, + runEmbeddedAgentMock, } from "./run.test-harness.js"; const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn(); @@ -144,7 +144,7 @@ function getMockCallArg( function expectEmbeddedRunFields(expected: Record): Record { return expectRecordFields( - getMockCallArg(runEmbeddedPiAgentMock, 0, 0, "embedded run"), + getMockCallArg(runEmbeddedAgentMock, 0, 0, "embedded run"), expected, "embedded run params", ); @@ -185,7 +185,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { mockRunCronFallbackPassthrough(); resolveCronDeliveryPlanMock.mockReturnValue(plan); await runCronIsolatedAgentTurn(makeParams()); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expectEmbeddedRunFields({ disableMessageTool: true, forceMessageTool: false, @@ -201,7 +201,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { mockRunCronFallbackPassthrough(); resolveCronDeliveryPlanMock.mockReturnValue(plan); await runCronIsolatedAgentTurn(makeParams()); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expectEmbeddedRunFields({ disableMessageTool: false, forceMessageTool: false, @@ -226,7 +226,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { }); expect(resolveDeliveryTargetMock).toHaveBeenCalledTimes(1); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expectEmbeddedRunFields({ disableMessageTool: false, forceMessageTool: false, @@ -242,7 +242,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { }) { mockRunCronFallbackPassthrough(); resolveCronDeliveryPlanMock.mockReturnValue(makeAnnounceDeliveryPlan()); - runEmbeddedPiAgentMock.mockResolvedValue(makeMessageToolRunResult(options.sentTargets)); + runEmbeddedAgentMock.mockResolvedValue(makeMessageToolRunResult(options.sentTargets)); const result = await runCronIsolatedAgentTurn({ ...makeParams(), @@ -385,7 +385,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { }); expect(resolveDeliveryTargetMock).not.toHaveBeenCalled(); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); const embeddedRun = expectEmbeddedRunFields({ disableMessageTool: false, forceMessageTool: false, @@ -422,7 +422,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { }), }); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expectEmbeddedRunFields({ disableMessageTool: false, forceMessageTool: false, @@ -466,7 +466,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { } as never, }); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expectEmbeddedRunFields({ disableMessageTool: false, messageChannel: "topicchat", @@ -503,7 +503,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { accountId: undefined, error: undefined, }); - runEmbeddedPiAgentMock.mockResolvedValue( + runEmbeddedAgentMock.mockResolvedValue( makeMessageToolRunResult([ { tool: "message", provider: "topicchat", to: "room#42", threadId: "42" }, ]), @@ -565,7 +565,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { }); expect(resolveDeliveryTargetMock).not.toHaveBeenCalled(); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); const embeddedRun = expectEmbeddedRunFields({ disableMessageTool: false, forceMessageTool: false, @@ -601,7 +601,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { await executor.runPrompt("send a message"); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expectEmbeddedRunFields({ messageChannel: "topicchat", agentAccountId: "ops", @@ -624,7 +624,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { await executor.runPrompt("send a message"); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expectEmbeddedRunFields({ messageChannel: "topicchat", agentAccountId: "ops", @@ -652,7 +652,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { job: makeAnnounceMessageToolJob(), }); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expectEmbeddedRunFields({ sourceReplyDeliveryMode: undefined, forceMessageTool: false, @@ -671,7 +671,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { job: makeAnnounceMessageToolJob(), }); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expect(expectEmbeddedRunFields({}).execOverrides).toBeUndefined(); }); @@ -691,7 +691,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { }), }); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expect(expectEmbeddedRunFields({}).execOverrides).toBeUndefined(); }); @@ -712,7 +712,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { await runCronIsolatedAgentTurn(makeParams()); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); expectEmbeddedRunFields({ disableMessageTool: false }); }); @@ -780,7 +780,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { it("does not dispatch announce delivery for fatal error payloads", async () => { mockRunCronFallbackPassthrough(); resolveCronDeliveryPlanMock.mockReturnValue(makeAnnounceDeliveryPlan()); - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [ { text: 'Codex error: {"type":"error","error":{"type":"server_error"}}', @@ -820,7 +820,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { it("cleans up deleteAfterRun sessions when suppressing fatal error announces", async () => { mockRunCronFallbackPassthrough(); resolveCronDeliveryPlanMock.mockReturnValue(makeAnnounceDeliveryPlan()); - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "provider failed", isError: true }], meta: { agentMeta: { usage: { input: 10, output: 20 } } }, }); @@ -866,7 +866,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { it("rewrites generic message provider to resolved channel in delivery trace", async () => { mockRunCronFallbackPassthrough(); resolveCronDeliveryPlanMock.mockReturnValue(makeAnnounceDeliveryPlan()); - runEmbeddedPiAgentMock.mockResolvedValue( + runEmbeddedAgentMock.mockResolvedValue( makeMessageToolRunResult([{ tool: "message", provider: "message", to: "123" }]), ); @@ -888,7 +888,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { mockRunCronFallbackPassthrough(); resolveCronDeliveryPlanMock.mockReturnValue(makeAnnounceDeliveryPlan({ accountId: "bot-a" })); resolveDeliveryTargetMock.mockResolvedValue(makeResolvedAnnounceTarget({ accountId: "bot-a" })); - runEmbeddedPiAgentMock.mockResolvedValue( + runEmbeddedAgentMock.mockResolvedValue( makeMessageToolRunResult([ { tool: "message", provider: "message", to: "123", accountId: "bot-a" }, ]), @@ -912,7 +912,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { mockRunCronFallbackPassthrough(); resolveCronDeliveryPlanMock.mockReturnValue(makeAnnounceDeliveryPlan({ accountId: "bot-a" })); resolveDeliveryTargetMock.mockResolvedValue(makeResolvedAnnounceTarget({ accountId: "bot-a" })); - runEmbeddedPiAgentMock.mockResolvedValue( + runEmbeddedAgentMock.mockResolvedValue( makeMessageToolRunResult([{ tool: "message", provider: "message", to: "123" }]), ); @@ -934,7 +934,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { mockRunCronFallbackPassthrough(); resolveCronDeliveryPlanMock.mockReturnValue(makeAnnounceDeliveryPlan({ accountId: "bot-a" })); resolveDeliveryTargetMock.mockResolvedValue(makeResolvedAnnounceTarget({ accountId: "bot-a" })); - runEmbeddedPiAgentMock.mockResolvedValue( + runEmbeddedAgentMock.mockResolvedValue( makeMessageToolRunResult([ { tool: "message", provider: "message", to: "123", accountId: "bot-b" }, ]), @@ -970,7 +970,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { mode: "implicit", error: new Error("sessionKey is required to resolve delivery.channel=last"), }); - runEmbeddedPiAgentMock.mockResolvedValue( + runEmbeddedAgentMock.mockResolvedValue( makeMessageToolRunResult([{ tool: "message", provider: "messagechat", to: "123" }]), ); @@ -1016,7 +1016,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { mode: "none", channel: "last", }); - runEmbeddedPiAgentMock.mockResolvedValue( + runEmbeddedAgentMock.mockResolvedValue( makeMessageToolRunResult([{ tool: "message", provider: "messagechat", to: "123" }]), ); @@ -1053,7 +1053,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { mockRunCronFallbackPassthrough(); mockPendingMessagePresentationWarningOutcome(); resolveCronDeliveryPlanMock.mockReturnValue(makeAnnounceDeliveryPlan()); - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "Final cron report" }, { text: "⚠️ ✉️ Message failed", isError: true }], meta: { agentMeta: { usage: { input: 10, output: 20 } } }, }); @@ -1077,7 +1077,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => { mockRunCronFallbackPassthrough(); mockPendingMessagePresentationWarningOutcome(); resolveCronDeliveryPlanMock.mockReturnValue({ requested: false, mode: "none" }); - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "Final cron report" }, { text: "⚠️ ✉️ Message failed", isError: true }], meta: { agentMeta: { usage: { input: 10, output: 20 } } }, }); @@ -1127,7 +1127,7 @@ describe("runCronIsolatedAgentTurn delivery instruction", () => { await runCronIsolatedAgentTurn(makeParams()); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); const prompt = expectEmbeddedRunPrompt(); expect(prompt).toContain("Use the message tool"); expect(prompt).toContain("will be delivered automatically"); @@ -1151,7 +1151,7 @@ describe("runCronIsolatedAgentTurn delivery instruction", () => { ), }); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); const prompt = expectEmbeddedRunPrompt(); expect(prompt).not.toContain("Use the message tool"); expect(prompt).toContain("Return your response as plain text"); @@ -1260,7 +1260,7 @@ describe("runCronIsolatedAgentTurn delivery instruction", () => { await runCronIsolatedAgentTurn(makeParams()); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); const prompt = expectEmbeddedRunPrompt(); expect(prompt).not.toContain("Return your response as plain text"); expect(prompt).not.toContain("it will be delivered automatically"); @@ -1280,7 +1280,7 @@ describe("runCronIsolatedAgentTurn delivery instruction", () => { await runCronIsolatedAgentTurn(makeParams()); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); const prompt = expectEmbeddedRunPrompt(); expect(prompt).not.toMatch(/\bsummary\b/i); }); diff --git a/src/cron/isolated-agent/run.payload-fallbacks.test.ts b/src/cron/isolated-agent/run.payload-fallbacks.test.ts index d6ff517af0d..b529f1b0114 100644 --- a/src/cron/isolated-agent/run.payload-fallbacks.test.ts +++ b/src/cron/isolated-agent/run.payload-fallbacks.test.ts @@ -11,7 +11,7 @@ import { resolveConfiguredModelRefMock, resolveAgentModelFallbacksOverrideMock, runCliAgentMock, - runEmbeddedPiAgentMock, + runEmbeddedAgentMock, runWithModelFallbackMock, } from "./run.test-harness.js"; @@ -38,7 +38,7 @@ function requireModelFallbackRequest(): { function requireEmbeddedRunRequest(): { modelFallbacksOverride?: string[]; } { - const request = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as + const request = runEmbeddedAgentMock.mock.calls[0]?.[0] as | { modelFallbacksOverride?: string[]; } @@ -170,8 +170,8 @@ describe("runCronIsolatedAgentTurn — payload.fallbacks", () => { "openai-codex/gpt-5.2", "zai/glm-5", ]); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - expect(runEmbeddedPiAgentMock.mock.calls[0]?.[0]).toMatchObject({ + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); + expect(runEmbeddedAgentMock.mock.calls[0]?.[0]).toMatchObject({ modelFallbacksOverride: ["openai-codex/gpt-5.2", "zai/glm-5"], }); }); diff --git a/src/cron/isolated-agent/run.session-key-isolation.test.ts b/src/cron/isolated-agent/run.session-key-isolation.test.ts index 8a6cb64a275..b3b27370c97 100644 --- a/src/cron/isolated-agent/run.session-key-isolation.test.ts +++ b/src/cron/isolated-agent/run.session-key-isolation.test.ts @@ -11,7 +11,7 @@ import { mockRunCronFallbackPassthrough, resolveCronSessionMock, runCliAgentMock, - runEmbeddedPiAgentMock, + runEmbeddedAgentMock, } from "./run.test-harness.js"; const runCronIsolatedAgentTurn = await loadRunCronIsolatedAgentTurn(); @@ -59,8 +59,8 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => { ) as { forceNew?: boolean; sessionKey?: string }; expect(sessionRequest.forceNew).toBe(true); expect(sessionRequest.sessionKey).toBe("agent:default:cron:daily-monitor"); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - const runRequest = requireFirstMockArg(runEmbeddedPiAgentMock, "runEmbeddedPiAgentMock") as { + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); + const runRequest = requireFirstMockArg(runEmbeddedAgentMock, "runEmbeddedAgentMock") as { sessionId?: string; sessionKey?: string; bootstrapContextMode?: string; @@ -95,8 +95,8 @@ describe("runCronIsolatedAgentTurn isolated session identity", () => { expect(result.status).toBe("ok"); expect(result.sessionKey).toBe("agent:default:project-alpha-monitor"); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); - const runRequest = requireFirstMockArg(runEmbeddedPiAgentMock, "runEmbeddedPiAgentMock") as { + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); + const runRequest = requireFirstMockArg(runEmbeddedAgentMock, "runEmbeddedAgentMock") as { sessionId?: string; sessionKey?: string; bootstrapContextMode?: string; diff --git a/src/cron/isolated-agent/run.test-harness.ts b/src/cron/isolated-agent/run.test-harness.ts index 2c34c6c6207..033d24050ad 100644 --- a/src/cron/isolated-agent/run.test-harness.ts +++ b/src/cron/isolated-agent/run.test-harness.ts @@ -51,7 +51,7 @@ export const resolveConfiguredModelRefMock = createMock(); export const resolveHooksGmailModelMock = createMock(); export const resolveThinkingDefaultMock = createMock(); export const runWithModelFallbackMock = createMock(); -export const runEmbeddedPiAgentMock = createMock(); +export const runEmbeddedAgentMock = createMock(); export const runCliAgentMock = createMock(); export const lookupContextTokensMock = createMock(); export const getCliSessionIdMock = createMock(); @@ -144,7 +144,7 @@ vi.mock("./run-model-catalog.runtime.js", () => ({ loadModelCatalog: loadModelCatalogMock, })); -vi.mock("./run-runtime-plugins.runtime.js", () => ({ +vi.mock("../../plugins/runtime-plugins.runtime.js", () => ({ ensureRuntimePluginsLoaded: ensureRuntimePluginsLoadedMock, })); @@ -196,7 +196,7 @@ vi.mock("./run-execution.runtime.js", () => ({ LiveSessionModelSwitchError, runWithModelFallback: runWithModelFallbackMock, isCliProvider: isCliProviderMock, - runEmbeddedPiAgent: runEmbeddedPiAgentMock, + runEmbeddedAgent: runEmbeddedAgentMock, countActiveDescendantRuns: countActiveDescendantRunsMock, listDescendantRunsForRequester: listDescendantRunsForRequesterMock, normalizeVerboseLevel: normalizeVerboseLevelMock, @@ -212,7 +212,7 @@ vi.mock("./run-auth-profile.runtime.js", () => ({ vi.mock("./run-embedded.runtime.js", () => ({ resolveFastModeState: resolveFastModeStateMock, resolveCronAgentLane: resolveCronAgentLaneMock, - runEmbeddedPiAgent: runEmbeddedPiAgentMock, + runEmbeddedAgent: runEmbeddedAgentMock, })); vi.mock("./run-subagent-registry.runtime.js", () => ({ @@ -224,7 +224,7 @@ vi.mock("../../agents/cli-runner.runtime.js", () => ({ setCliSessionId: vi.fn(), })); -vi.mock("../../agents/pi-bundle-mcp-tools.js", () => ({ +vi.mock("../../agents/agent-bundle-mcp-tools.js", () => ({ retireSessionMcpRuntime: retireSessionMcpRuntimeMock, })); @@ -417,8 +417,8 @@ function resetRunExecutionMocks(): void { registerAgentRunContextMock.mockReturnValue(undefined); runWithModelFallbackMock.mockReset(); runWithModelFallbackMock.mockResolvedValue(makeDefaultModelFallbackResult()); - runEmbeddedPiAgentMock.mockReset(); - runEmbeddedPiAgentMock.mockResolvedValue(makeDefaultEmbeddedResult()); + runEmbeddedAgentMock.mockReset(); + runEmbeddedAgentMock.mockResolvedValue(makeDefaultEmbeddedResult()); runCliAgentMock.mockReset(); getCliSessionIdMock.mockReturnValue(undefined); countActiveDescendantRunsMock.mockReset(); diff --git a/src/cron/isolated-agent/run.tools-allow.test.ts b/src/cron/isolated-agent/run.tools-allow.test.ts index 3cabba059da..09eb7881805 100644 --- a/src/cron/isolated-agent/run.tools-allow.test.ts +++ b/src/cron/isolated-agent/run.tools-allow.test.ts @@ -4,7 +4,7 @@ import { loadRunCronIsolatedAgentTurn, resetRunCronIsolatedAgentTurnHarness, resolveDeliveryTargetMock, - runEmbeddedPiAgentMock, + runEmbeddedAgentMock, runWithModelFallbackMock, } from "./run.test-harness.js"; @@ -49,14 +49,14 @@ function requireEmbeddedAgentCall(): { jobId?: string; toolsAllow?: string[]; } { - const call = runEmbeddedPiAgentMock.mock.calls[0]?.[0] as + const call = runEmbeddedAgentMock.mock.calls[0]?.[0] as | { jobId?: string; toolsAllow?: string[]; } | undefined; if (!call) { - throw new Error("Expected embedded PI agent call for toolsAllow passthrough"); + throw new Error("Expected embedded OpenClaw agent call for toolsAllow passthrough"); } return call; } @@ -95,7 +95,7 @@ describe("runCronIsolatedAgentTurn toolsAllow passthrough", () => { async () => { await runCronIsolatedAgentTurn(makeParamsWithToolsAllow(["cron"])); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); const call = requireEmbeddedAgentCall(); expect(call.jobId).toBe("tools-allow"); expect(call.toolsAllow).toEqual(["cron"]); @@ -108,7 +108,7 @@ describe("runCronIsolatedAgentTurn toolsAllow passthrough", () => { async () => { await runCronIsolatedAgentTurn(makeParamsWithToolsAllow([" CRON "])); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); const call = requireEmbeddedAgentCall(); expect(call.jobId).toBe("tools-allow"); expect(call.toolsAllow).toEqual([" CRON "]); @@ -121,7 +121,7 @@ describe("runCronIsolatedAgentTurn toolsAllow passthrough", () => { async () => { await runCronIsolatedAgentTurn(makeParamsWithToolsAllow(["maniple__check_idle_workers"])); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1); + expect(runEmbeddedAgentMock).toHaveBeenCalledTimes(1); const call = requireEmbeddedAgentCall(); expect(call.toolsAllow).toEqual(["maniple__check_idle_workers"]); }, diff --git a/src/cron/isolated-agent/run.ts b/src/cron/isolated-agent/run.ts index ee383b4170b..65ee0623579 100644 --- a/src/cron/isolated-agent/run.ts +++ b/src/cron/isolated-agent/run.ts @@ -1,7 +1,7 @@ +import { retireSessionMcpRuntime } from "../../agents/agent-bundle-mcp-tools.js"; import { hasAnyAuthProfileStoreSource } from "../../agents/auth-profiles/source-check.js"; import { resolveAgentHarnessPolicy } from "../../agents/harness/selection.js"; import { listOpenAIAuthProfileProvidersForAgentRuntime } from "../../agents/openai-codex-routing.js"; -import { retireSessionMcpRuntime } from "../../agents/pi-bundle-mcp-tools.js"; import type { SkillSnapshot } from "../../agents/skills.js"; import { expandToolGroups, normalizeToolName } from "../../agents/tool-policy.js"; import type { ThinkLevel } from "../../auto-reply/thinking.js"; @@ -111,7 +111,7 @@ const cronModelPreflightRuntimeLoader = createLazyImportLoader( () => import("./model-preflight.runtime.js"), ); const runtimePluginsLoader = createLazyImportLoader( - () => import("./run-runtime-plugins.runtime.js"), + () => import("../../plugins/runtime-plugins.runtime.js"), ); async function loadSessionStoreRuntime() { diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index a221849448a..72fcfe66c0d 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -1,6 +1,5 @@ +import { formatEmbeddedAgentExecutionPhase } from "../../agents/embedded-agent-runner/execution-phase.js"; import { resolveFailoverReasonFromError } from "../../agents/failover-error.js"; -import { formatEmbeddedAgentExecutionPhase } from "../../agents/pi-embedded-runner/execution-phase.js"; -import { resolveCronMaxConcurrentRuns } from "../../config/cron-limits.js"; import { readSessionEntry } from "../../config/sessions/store-load.js"; import type { SessionEntry } from "../../config/sessions/types.js"; import type { CronConfig } from "../../config/types.cron.js"; @@ -62,7 +61,6 @@ import { import { locked } from "./locked.js"; import type { CronEvent, CronServiceState } from "./state.js"; import { ensureLoaded, persist } from "./store.js"; -import { CRON_TASK_RUNNING_PROGRESS_SUMMARY } from "./task-ledger.js"; import { resolveCronJobTimeoutMs } from "./timeout-policy.js"; export { DEFAULT_JOB_TIMEOUT_MS } from "./timeout-policy.js"; @@ -366,7 +364,11 @@ async function cleanupTimedOutCronAgentRun( } function resolveRunConcurrency(state: CronServiceState): number { - return resolveCronMaxConcurrentRuns(state.deps.cronConfig); + const raw = state.deps.cronConfig?.maxConcurrentRuns; + if (typeof raw !== "number" || !Number.isFinite(raw)) { + return 1; + } + return Math.max(1, Math.floor(raw)); } function timeoutErrorMessage(execution?: CronAgentExecutionStarted): string { const phase = formatCronAgentExecutionPhase(execution); @@ -510,7 +512,6 @@ function tryCreateCronTaskRun(params: { notifyPolicy: "silent", startedAt: params.startedAt, lastEventAt: params.startedAt, - progressSummary: CRON_TASK_RUNNING_PROGRESS_SUMMARY, }); return runId; } catch (error) { diff --git a/src/cron/types.ts b/src/cron/types.ts index 21dffb4b2b3..d7758077286 100644 --- a/src/cron/types.ts +++ b/src/cron/types.ts @@ -1,5 +1,5 @@ -import type { FailoverReason } from "../agents/pi-embedded-helpers/types.js"; -import type { EmbeddedAgentExecutionPhase } from "../agents/pi-embedded-runner/execution-phase.js"; +import type { FailoverReason } from "../agents/embedded-agent-helpers/types.js"; +import type { EmbeddedAgentExecutionPhase } from "../agents/embedded-agent-runner/execution-phase.js"; import type { ChannelId } from "../channels/plugins/types.public.js"; import type { HookExternalContentSource } from "../security/external-content.js"; import type { CronJobBase } from "./types-shared.js"; diff --git a/src/extensionAPI.ts b/src/extensionAPI.ts index 9e354998fa6..ab19e11f85d 100644 --- a/src/extensionAPI.ts +++ b/src/extensionAPI.ts @@ -24,7 +24,11 @@ export { resolveAgentDir, resolveAgentWorkspaceDir } from "./agents/agent-scope. export { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./agents/defaults.js"; export { resolveAgentIdentity } from "./agents/identity.js"; export { resolveThinkingDefault } from "./agents/model-selection.js"; -export { runEmbeddedPiAgent } from "./agents/pi-embedded.js"; +export { + runEmbeddedAgent, + /** @deprecated Use runEmbeddedAgent. */ + runEmbeddedAgent as runEmbeddedPiAgent, +} from "./agents/embedded-agent.js"; export { resolveAgentTimeoutMs } from "./agents/timeout.js"; export { ensureAgentWorkspace } from "./agents/workspace.js"; export { diff --git a/src/flows/doctor-core-checks.ts b/src/flows/doctor-core-checks.ts index 6c9edeaa62f..d259eda9f7d 100644 --- a/src/flows/doctor-core-checks.ts +++ b/src/flows/doctor-core-checks.ts @@ -288,7 +288,7 @@ const bootstrapSizeCheck: HealthCheck = { await import("../agents/bootstrap-budget.js"); const { resolveBootstrapContextForRun } = await import("../agents/bootstrap-files.js"); const { resolveBootstrapMaxChars, resolveBootstrapTotalMaxChars } = - await import("../agents/pi-embedded-helpers.js"); + await import("../agents/embedded-agent-helpers.js"); const workspaceDir = resolveAgentWorkspaceDir(ctx.cfg, resolveDefaultAgentId(ctx.cfg)); const { bootstrapFiles, contextFiles } = await resolveBootstrapContextForRun({ workspaceDir, diff --git a/src/gateway/gateway-cli-backend.live-helpers.ts b/src/gateway/gateway-cli-backend.live-helpers.ts index 6976babedba..122f085f12b 100644 --- a/src/gateway/gateway-cli-backend.live-helpers.ts +++ b/src/gateway/gateway-cli-backend.live-helpers.ts @@ -96,7 +96,7 @@ export function resolveCliBackendLiveModelSelection(params: { cliModelKey: modelKey, configModelKey: modelKey, configModelSwitchTarget: params.modelSwitchTarget, - agentRuntime: { id: "pi" }, + agentRuntime: { id: "openclaw" }, }; } diff --git a/src/gateway/gateway-models.profiles.live.test.ts b/src/gateway/gateway-models.profiles.live.test.ts index 6317a8fca97..cff7cfc46cd 100644 --- a/src/gateway/gateway-models.profiles.live.test.ts +++ b/src/gateway/gateway-models.profiles.live.test.ts @@ -12,9 +12,10 @@ import { type KnownProvider, type Model, type ModelThinkingLevel, -} from "@earendil-works/pi-ai"; +} from "openclaw/plugin-sdk/llm"; import { afterEach, describe, expect, it } from "vitest"; import { renderCatNoncePngBase64 } from "../../test/helpers/live-image-probe.js"; +import { discoverAuthStorage, discoverModels } from "../agents/agent-model-discovery.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentDir } from "../agents/agent-scope.js"; import { ensureAuthProfileStore, @@ -44,7 +45,6 @@ import { getApiKeyForModel, resolveEnvApiKey } from "../agents/model-auth.js"; import { normalizeProviderId } from "../agents/model-selection.js"; import { shouldSuppressBuiltInModel } from "../agents/model-suppression.js"; import { ensureOpenClawModelsJson } from "../agents/models-config.js"; -import { discoverAuthStorage, discoverModels } from "../agents/pi-model-discovery.js"; import { STREAM_ERROR_FALLBACK_TEXT } from "../agents/stream-message-shared.js"; import { clearRuntimeConfigSnapshot, getRuntimeConfig } from "../config/io.js"; import type { ModelsConfig, ModelProviderConfig, OpenClawConfig } from "../config/types.js"; @@ -858,7 +858,7 @@ describe("resolveGatewayLiveMaxModels", () => { }); }); -function createGatewayLiveTestModel(provider: string, id: string): Model { +function createGatewayLiveTestModel(provider: string, id: string): Model { return { provider, id, @@ -869,7 +869,7 @@ function createGatewayLiveTestModel(provider: string, id: string): Model { contextWindow: 1_000, maxTokens: 100, reasoning: false, - } as Model; + } as Model; } describe("resolveExplicitLiveModelCandidates", () => { @@ -1833,7 +1833,7 @@ async function requestGatewayAgentText(params: { type GatewayModelSuiteParams = { label: string; cfg: OpenClawConfig; - candidates: Array>; + candidates: Array; allowNotFoundSkip: boolean; extraToolProbes: boolean; extraImageProbes: boolean; @@ -1842,8 +1842,8 @@ type GatewayModelSuiteParams = { }; type LiveModelRegistry = { - find(provider: string, modelId: string): Model | null | undefined; - getAll(): Array>; + find(provider: string, modelId: string): Model | null | undefined; + getAll(): Array; }; function resolveKnownProvider(provider: string): KnownProvider | undefined { @@ -1855,7 +1855,7 @@ function toGatewayLiveModel(params: { provider: string; providerConfig: ModelProviderConfig; modelConfig: NonNullable[number]; -}): Model | null { +}): Model | null { const id = params.modelConfig.id?.trim(); const api = params.modelConfig.api ?? params.providerConfig.api; const baseUrl = params.modelConfig.baseUrl ?? params.providerConfig.baseUrl; @@ -1888,7 +1888,7 @@ function toGatewayLiveModel(params: { async function loadProviderScopedConfiguredModels(params: { agentDir: string; providerList: readonly string[]; -}): Promise>> { +}): Promise> { const modelsPath = path.join(params.agentDir, "models.json"); let parsed: { providers?: Record }; try { @@ -1900,7 +1900,7 @@ async function loadProviderScopedConfiguredModels(params: { } const providers = parsed.providers ?? {}; - const models: Array> = []; + const models: Array = []; const seen = new Set(); for (const rawProvider of params.providerList) { const normalizedProvider = normalizeProviderId(rawProvider); @@ -1927,8 +1927,8 @@ async function loadProviderScopedConfiguredModels(params: { return models; } -function loadProviderScopedBuiltInModels(providerList: readonly string[]): Array> { - const models: Array> = []; +function loadProviderScopedBuiltInModels(providerList: readonly string[]): Array { + const models: Array = []; const seen = new Set(); for (const rawProvider of providerList) { const provider = normalizeProviderId(rawProvider); @@ -1954,7 +1954,7 @@ function loadProviderScopedBuiltInModels(providerList: readonly string[]): Array async function loadProviderScopedModels(params: { agentDir: string; providerList: readonly string[]; -}): Promise>> { +}): Promise> { const configured = await loadProviderScopedConfiguredModels(params); if (configured.length > 0) { return configured; @@ -1962,7 +1962,7 @@ async function loadProviderScopedModels(params: { return loadProviderScopedBuiltInModels(params.providerList); } -function createStaticLiveModelRegistry(models: Array>): LiveModelRegistry { +function createStaticLiveModelRegistry(models: Array): LiveModelRegistry { return { find(provider, modelId) { const normalizedProvider = normalizeProviderId(provider); @@ -2009,11 +2009,11 @@ function resolveExplicitLiveModelCandidates(params: { modelFilter: Set | null; providerFilter: Set | null; targetMatcher: ReturnType; -}): Array> | null { +}): Array | null { if (!params.modelFilter || params.modelFilter.size === 0) { return null; } - const candidates: Array> = []; + const candidates: Array = []; const seen = new Set(); for (const raw of params.modelFilter) { const ref = parseExplicitLiveModelRef(raw, params.providerFilter); @@ -2041,7 +2041,7 @@ function resolveExplicitLiveModelCandidates(params: { function resolveGatewayLiveModelThinkingLevel(params: { cfg: OpenClawConfig; - model: Model; + model: Model; requestedLevel: string; }): string { const { model, requestedLevel } = params; @@ -2075,7 +2075,7 @@ function resolveGatewayLiveModelThinkingLevel(params: { function buildLiveGatewayConfig(params: { cfg: OpenClawConfig; - candidates: Array>; + candidates: Array; providerOverrides?: Record; }): OpenClawConfig { const providerOverrides = params.providerOverrides ?? {}; @@ -2205,7 +2205,6 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { disableBonjour: process.env.OPENCLAW_DISABLE_BONJOUR, logLevel: process.env.OPENCLAW_LOG_LEVEL, agentDir: process.env.OPENCLAW_AGENT_DIR, - piAgentDir: process.env.PI_CODING_AGENT_DIR, stateDir: process.env.OPENCLAW_STATE_DIR, }; let tempAgentDir: string | undefined; @@ -2246,7 +2245,6 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { saveAuthProfileStore(sanitizedStore, tempSessionAgentDir); } process.env.OPENCLAW_AGENT_DIR = tempAgentDir; - process.env.PI_CODING_AGENT_DIR = tempAgentDir; const workspaceDir = resolveAgentWorkspaceDir(params.cfg, agentId); await fs.mkdir(workspaceDir, { recursive: true }); @@ -2972,7 +2970,6 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { process.env.OPENCLAW_DISABLE_BONJOUR = previous.disableBonjour; process.env.OPENCLAW_LOG_LEVEL = previous.logLevel; process.env.OPENCLAW_AGENT_DIR = previous.agentDir; - process.env.PI_CODING_AGENT_DIR = previous.piAgentDir; process.env.OPENCLAW_STATE_DIR = previous.stateDir; } } @@ -3015,7 +3012,7 @@ describeLive("gateway live (dev agent, profile keys)", () => { }); let authProfileStore: AuthProfileStore | undefined; let modelRegistry: LiveModelRegistry; - let all: Array>; + let all: Array; if (providerScopedModelProviders) { logProgress("[all-models] loading provider-scoped model refs"); all = await withGatewayLiveSetupTimeout( @@ -3098,7 +3095,7 @@ describeLive("gateway live (dev agent, profile keys)", () => { wantedCount: wanted.length, }); - const candidates: Array> = []; + const candidates: Array = []; const skipped: Array<{ model: string; error: string }> = []; for (const model of wanted) { if (shouldSuppressBuiltInModel({ provider: model.provider, id: model.id })) { @@ -3231,8 +3228,8 @@ describeLive("gateway live (dev agent, profile keys)", () => { const agentDir = resolveDefaultAgentDir(cfg); const authStorage = discoverAuthStorage(agentDir); const modelRegistry = discoverModels(authStorage, agentDir); - const anthropic = modelRegistry.find("anthropic", "claude-opus-4-6") as Model | null; - const zai = modelRegistry.find("zai", "glm-5.1") as Model | null; + const anthropic = modelRegistry.find("anthropic", "claude-opus-4-6") as Model | null; + const zai = modelRegistry.find("zai", "glm-5.1") as Model | null; if (!anthropic || !zai) { return; diff --git a/src/gateway/mcp-http.handlers.ts b/src/gateway/mcp-http.handlers.ts index d4da84e4d5c..aad53ba69fb 100644 --- a/src/gateway/mcp-http.handlers.ts +++ b/src/gateway/mcp-http.handlers.ts @@ -1,5 +1,5 @@ import crypto from "node:crypto"; -import { runBeforeToolCallHook, type HookContext } from "../agents/pi-tools.before-tool-call.js"; +import { runBeforeToolCallHook, type HookContext } from "../agents/agent-tools.before-tool-call.js"; import { formatErrorMessage } from "../infra/errors.js"; import { MCP_LOOPBACK_SERVER_NAME, diff --git a/src/gateway/mcp-http.test.ts b/src/gateway/mcp-http.test.ts index a4efeb8482d..179d2dbf662 100644 --- a/src/gateway/mcp-http.test.ts +++ b/src/gateway/mcp-http.test.ts @@ -72,7 +72,7 @@ vi.mock("../config/sessions.js", () => ({ resolveMainSessionKey: () => "agent:main:main", })); -vi.mock("../agents/pi-tools.before-tool-call.js", () => ({ +vi.mock("../agents/agent-tools.before-tool-call.js", () => ({ runBeforeToolCallHook: (...args: Parameters) => runBeforeToolCallHookMock(...args), })); diff --git a/src/gateway/openai-compat-errors.ts b/src/gateway/openai-compat-errors.ts index 5ca599436ea..a24b14e0c85 100644 --- a/src/gateway/openai-compat-errors.ts +++ b/src/gateway/openai-compat-errors.ts @@ -1,5 +1,5 @@ +import type { FailoverReason } from "../agents/embedded-agent-helpers/types.js"; import { describeFailoverError, resolveFailoverStatus } from "../agents/failover-error.js"; -import type { FailoverReason } from "../agents/pi-embedded-helpers/types.js"; export type OpenAiCompatError = { status: number; diff --git a/src/gateway/openai-http.test.ts b/src/gateway/openai-http.test.ts index 9d4bc768e42..65df8b542ce 100644 --- a/src/gateway/openai-http.test.ts +++ b/src/gateway/openai-http.test.ts @@ -2,13 +2,13 @@ import fs from "node:fs/promises"; import http from "node:http"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; -import { FailoverError } from "../agents/failover-error.js"; +import { createClientToolNameConflictError } from "../agents/agent-tool-definition-adapter.js"; import { createStubSessionHarness, emitAssistantTextDelta, -} from "../agents/pi-embedded-subscribe.e2e-harness.js"; -import { subscribeEmbeddedPiSession } from "../agents/pi-embedded-subscribe.js"; -import { createClientToolNameConflictError } from "../agents/pi-tool-definition-adapter.js"; +} from "../agents/embedded-agent-subscribe.e2e-harness.js"; +import { subscribeEmbeddedAgentSession } from "../agents/embedded-agent-subscribe.js"; +import { FailoverError } from "../agents/failover-error.js"; import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js"; import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js"; import { emitAgentEvent } from "../infra/agent-events.js"; @@ -1066,7 +1066,7 @@ describe("OpenAI-compatible HTTP API (e2e)", () => { agentCommand.mockImplementationOnce((async (opts: unknown) => { const runId = (opts as { runId?: string } | undefined)?.runId ?? ""; const { session, emit } = createStubSessionHarness(); - subscribeEmbeddedPiSession({ session, runId }); + subscribeEmbeddedAgentSession({ session, runId }); emit({ type: "message_start", message: { role: "assistant" } }); for (const delta of ["<", "final>Title\n", "Line one\nLine two"]) { emitAssistantTextDelta({ emit, delta }); diff --git a/src/gateway/openai-http.ts b/src/gateway/openai-http.ts index 05392cbc9cf..81125670180 100644 --- a/src/gateway/openai-http.ts +++ b/src/gateway/openai-http.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; import type { AgentStreamParams, ClientToolDefinition } from "../agents/command/shared-types.js"; import type { ImageContent } from "../agents/command/types.js"; -import { isClientToolNameConflictError } from "../agents/pi-tool-definition-adapter.js"; +import { isClientToolNameConflictError } from "../agents/agent-tool-definition-adapter.js"; import { STREAM_ERROR_FALLBACK_TEXT } from "../agents/stream-message-shared.js"; import { hasNonzeroUsage, diff --git a/src/gateway/openresponses-http.test.ts b/src/gateway/openresponses-http.test.ts index f495037648f..4d5ebea4ba7 100644 --- a/src/gateway/openresponses-http.test.ts +++ b/src/gateway/openresponses-http.test.ts @@ -2,8 +2,8 @@ import fs from "node:fs/promises"; import http from "node:http"; import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { createClientToolNameConflictError } from "../agents/agent-tool-definition-adapter.js"; import { FailoverError } from "../agents/failover-error.js"; -import { createClientToolNameConflictError } from "../agents/pi-tool-definition-adapter.js"; import { HISTORY_CONTEXT_MARKER } from "../auto-reply/reply/history.js"; import { CURRENT_MESSAGE_MARKER } from "../auto-reply/reply/mentions.js"; import { emitAgentEvent } from "../infra/agent-events.js"; diff --git a/src/gateway/openresponses-http.ts b/src/gateway/openresponses-http.ts index 49da9305ee1..0dd3617b5e2 100644 --- a/src/gateway/openresponses-http.ts +++ b/src/gateway/openresponses-http.ts @@ -8,9 +8,9 @@ import { createHash, randomUUID } from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; +import { isClientToolNameConflictError } from "../agents/agent-tool-definition-adapter.js"; import type { ImageContent } from "../agents/command/types.js"; -import type { ClientToolDefinition } from "../agents/pi-embedded-runner/run/params.js"; -import { isClientToolNameConflictError } from "../agents/pi-tool-definition-adapter.js"; +import type { ClientToolDefinition } from "../agents/embedded-agent-runner/run/params.js"; import { createDefaultDeps } from "../cli/deps.js"; import type { CliDeps } from "../cli/deps.types.js"; import { agentCommandFromIngress } from "../commands/agent.js"; diff --git a/src/gateway/protocol/schema/agents-models-skills.ts b/src/gateway/protocol/schema/agents-models-skills.ts index f7496f2e9c3..25a880e8a7c 100644 --- a/src/gateway/protocol/schema/agents-models-skills.ts +++ b/src/gateway/protocol/schema/agents-models-skills.ts @@ -43,7 +43,7 @@ export const AgentSummarySchema = Type.Object( Type.Object( { id: NonEmptyString, - fallback: Type.Optional(Type.Union([Type.Literal("pi"), Type.Literal("none")])), + fallback: Type.Optional(Type.Union([Type.Literal("openclaw"), Type.Literal("none")])), source: Type.Union([ Type.Literal("env"), Type.Literal("agent"), diff --git a/src/gateway/server-close.test.ts b/src/gateway/server-close.test.ts index 113cdec5ee4..6ac431113da 100644 --- a/src/gateway/server-close.test.ts +++ b/src/gateway/server-close.test.ts @@ -44,16 +44,16 @@ vi.mock("../agents/harness/registry.js", () => ({ disposeRegisteredAgentHarnesses: mocks.disposeAgentHarnesses, })); -vi.mock("../agents/pi-bundle-mcp-tools.js", async () => ({ - ...(await vi.importActual( - "../agents/pi-bundle-mcp-tools.js", +vi.mock("../agents/agent-bundle-mcp-tools.js", async () => ({ + ...(await vi.importActual( + "../agents/agent-bundle-mcp-tools.js", )), disposeAllSessionMcpRuntimes: mocks.disposeAllSessionMcpRuntimes, })); -vi.mock("../agents/pi-bundle-lsp-runtime.js", async () => ({ - ...(await vi.importActual( - "../agents/pi-bundle-lsp-runtime.js", +vi.mock("../agents/agent-bundle-lsp-runtime.js", async () => ({ + ...(await vi.importActual( + "../agents/agent-bundle-lsp-runtime.js", )), disposeAllBundleLspRuntimes: mocks.disposeAllBundleLspRuntimes, })); diff --git a/src/gateway/server-close.ts b/src/gateway/server-close.ts index b1073565e1a..9615cf30239 100644 --- a/src/gateway/server-close.ts +++ b/src/gateway/server-close.ts @@ -1,7 +1,7 @@ import type { Server as HttpServer } from "node:http"; import type { WebSocketServer } from "ws"; +import { disposeAllSessionMcpRuntimes } from "../agents/agent-bundle-mcp-tools.js"; import { disposeRegisteredAgentHarnesses } from "../agents/harness/registry.js"; -import { disposeAllSessionMcpRuntimes } from "../agents/pi-bundle-mcp-tools.js"; import { type ChannelId, listChannelPlugins } from "../channels/plugins/index.js"; import { createInternalHookEvent, triggerInternalHook } from "../hooks/internal-hooks.js"; import type { HeartbeatRunner } from "../infra/heartbeat-runner.js"; @@ -319,7 +319,7 @@ async function disposeRuntimeWithShutdownGrace(params: { } async function disposeAllBundleLspRuntimesOnDemand(): Promise { - const { disposeAllBundleLspRuntimes } = await import("../agents/pi-bundle-lsp-runtime.js"); + const { disposeAllBundleLspRuntimes } = await import("../agents/agent-bundle-lsp-runtime.js"); await disposeAllBundleLspRuntimes(); } diff --git a/src/gateway/server-cron.ts b/src/gateway/server-cron.ts index 695f4e2086b..855017472a0 100644 --- a/src/gateway/server-cron.ts +++ b/src/gateway/server-cron.ts @@ -1,5 +1,5 @@ import { resolveDefaultAgentId } from "../agents/agent-scope.js"; -import { abortAndDrainEmbeddedPiRun } from "../agents/pi-embedded.js"; +import { abortAndDrainEmbeddedAgentRun } from "../agents/embedded-agent.js"; import { cleanupBrowserSessionsForLifecycleEnd } from "../browser-lifecycle-cleanup.js"; import type { CliDeps } from "../cli/deps.types.js"; import { getRuntimeConfig } from "../config/io.js"; @@ -387,7 +387,7 @@ export function buildGatewayCronService(params: { if (!execution?.sessionId) { return; } - const result = await abortAndDrainEmbeddedPiRun({ + const result = await abortAndDrainEmbeddedAgentRun({ sessionId: execution.sessionId, sessionKey: execution.sessionKey, settleMs: 15_000, diff --git a/src/gateway/server-methods/AGENTS.md b/src/gateway/server-methods/AGENTS.md index c2269f5707c..c6839330f7b 100644 --- a/src/gateway/server-methods/AGENTS.md +++ b/src/gateway/server-methods/AGENTS.md @@ -1,3 +1,3 @@ # Gateway Server Methods Notes -- Pi session transcripts are a `parentId` chain/DAG; never append Pi `type: "message"` entries via raw JSONL writes (missing `parentId` can sever the leaf path and break compaction/history). Always write transcript messages via `SessionManager.appendMessage(...)` (or a wrapper that uses it). +- agent session transcripts are a `parentId` chain/DAG; never append raw `type: "message"` entries via JSONL writes (missing `parentId` can sever the leaf path and break compaction/history). Always write transcript messages via `SessionManager.appendMessage(...)` (or a wrapper that uses it). diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 08422b5472e..568919e2545 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -5,6 +5,7 @@ import { resolveDefaultAgentId, resolveAgentWorkspaceDir, } from "../../agents/agent-scope.js"; +import { resolveTrustedGroupId } from "../../agents/agent-tools.policy.js"; import { consumeExecApprovalFollowupRuntimeHandoff, parseExecApprovalFollowupApprovalId, @@ -16,7 +17,6 @@ import { } from "../../agents/identity-avatar.js"; import { AGENT_INTERNAL_EVENT_TYPE_TASK_COMPLETION } from "../../agents/internal-event-contract.js"; import type { AgentInternalEvent } from "../../agents/internal-events.js"; -import { resolveTrustedGroupId } from "../../agents/pi-tools.policy.js"; import { resolveProviderIdForAuth } from "../../agents/provider-auth-aliases.js"; import { normalizeAgentRunTimeoutPhase, diff --git a/src/gateway/server-methods/chat-transcript-inject.ts b/src/gateway/server-methods/chat-transcript-inject.ts index 6fdc4912e2c..87025a47747 100644 --- a/src/gateway/server-methods/chat-transcript-inject.ts +++ b/src/gateway/server-methods/chat-transcript-inject.ts @@ -1,4 +1,4 @@ -import type { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { SessionManager } from "../../agents/sessions/index.js"; import { appendSessionTranscriptMessage } from "../../config/sessions/transcript-append.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { formatErrorMessage } from "../../infra/errors.js"; @@ -87,7 +87,7 @@ export async function appendInjectedAssistantMessageToTranscript(params: { { role: "assistant" } >["content"], timestamp: now, - // Pi stopReason is a strict enum; this is not model output, but we still store it as a + // stopReason is a strict runner enum; this is not model output, but we still store it as a // normal assistant message so it participates in the session parentId chain. stopReason: "stop", usage, diff --git a/src/gateway/server-methods/chat.abort-persistence.test.ts b/src/gateway/server-methods/chat.abort-persistence.test.ts index 896e0f472b1..0171bf87d44 100644 --- a/src/gateway/server-methods/chat.abort-persistence.test.ts +++ b/src/gateway/server-methods/chat.abort-persistence.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent"; +import { CURRENT_SESSION_VERSION } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createActiveRun, diff --git a/src/gateway/server-methods/chat.directive-tags.test.ts b/src/gateway/server-methods/chat.directive-tags.test.ts index f2ba92dde35..a37d03a88f4 100644 --- a/src/gateway/server-methods/chat.directive-tags.test.ts +++ b/src/gateway/server-methods/chat.directive-tags.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent"; +import { CURRENT_SESSION_VERSION } from "openclaw/plugin-sdk/agent-sessions"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { ModelCatalogEntry } from "../../agents/model-catalog.types.js"; import { setReplyPayloadMetadata } from "../../auto-reply/reply-payload.js"; @@ -1038,7 +1038,7 @@ describe("chat directive tag stripping for non-streaming final payloads", () => update.message !== null && (update.message as { role?: unknown }).role === "assistant", ); - // Agent-run delivery is a live projection; Pi message_end owns persisted + // Agent-run delivery is a live projection; message_end owns persisted // assistant transcript entries, including stale media/text final payloads. expect(assistantUpdates).toStrictEqual([]); const transcriptLines = readTranscriptJsonLines(mockState.transcriptPath); @@ -1086,7 +1086,7 @@ describe("chat directive tag stripping for non-streaming final payloads", () => (update.message as { role?: unknown }).role === "assistant", ); // Normal agent-run final text must not be mirrored into JSONL by WebChat; - // Pi persists the model-visible assistant turn from message_end. + // The agent runtime persists the model-visible assistant turn from message_end. expect(assistantUpdates).toStrictEqual([]); const transcriptLines = readTranscriptJsonLines(mockState.transcriptPath); const assistantEntries = transcriptLines.filter( diff --git a/src/gateway/server-methods/chat.inject.parentid.test.ts b/src/gateway/server-methods/chat.inject.parentid.test.ts index cc1db17ea37..298101e669f 100644 --- a/src/gateway/server-methods/chat.inject.parentid.test.ts +++ b/src/gateway/server-methods/chat.inject.parentid.test.ts @@ -17,7 +17,7 @@ function readTranscriptLines(transcriptPath: string): string[] { // Guardrail: Gateway-injected assistant transcript messages must attach to the // current leaf with a `parentId` and must not sever compaction history. describe("gateway chat.inject transcript writes", () => { - it("appends a Pi session entry that includes parentId", async () => { + it("appends a agent session entry that includes parentId", async () => { const { dir, transcriptPath } = createTranscriptFixtureSync({ prefix: "openclaw-chat-inject-", sessionId: "sess-1", diff --git a/src/gateway/server-methods/chat.test-helpers.ts b/src/gateway/server-methods/chat.test-helpers.ts index 69fc5399b76..3554cfb8498 100644 --- a/src/gateway/server-methods/chat.test-helpers.ts +++ b/src/gateway/server-methods/chat.test-helpers.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent"; +import { CURRENT_SESSION_VERSION } from "../../agents/sessions/index.js"; export function createTranscriptFixtureSync(params: { prefix: string; diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 7235abd8f36..350a059a73e 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -1,8 +1,6 @@ import { createHash } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent"; import { buildTtsSupplementMediaPayload, getReplyPayloadTtsSupplement, @@ -11,9 +9,11 @@ import { } from "openclaw/plugin-sdk/reply-payload"; import { resolveAgentWorkspaceDir, resolveSessionAgentId } from "../../agents/agent-scope.js"; import { runAgentHarnessBeforeMessageWriteHook } from "../../agents/harness/hook-helpers.js"; -import { rewriteTranscriptEntriesInSessionFile } from "../../agents/pi-embedded-runner/transcript-rewrite.js"; +import { rewriteTranscriptEntriesInSessionFile } from "../../agents/embedded-agent-runner/transcript-rewrite.js"; import { resolveProviderIdForAuth } from "../../agents/provider-auth-aliases.js"; +import type { AgentMessage } from "../../agents/runtime/index.js"; import { ensureSandboxWorkspaceForSession } from "../../agents/sandbox/context.js"; +import { CURRENT_SESSION_VERSION } from "../../agents/sessions/index.js"; import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; import { dispatchInboundMessage } from "../../auto-reply/dispatch.js"; import { getReplyPayloadMetadata, type ReplyPayload } from "../../auto-reply/reply-payload.js"; @@ -3068,10 +3068,10 @@ export const chatHandlers: GatewayRequestHandlers = { } let broadcastedSourceReplyFinal = false; // WebChat persistence has two owners. Agent runs persist model-visible turns - // through Pi's SessionManager; this dispatcher only owns live delivery payloads. + // through OpenClaw runtime's SessionManager; this dispatcher only owns live delivery payloads. // Do not blindly mirror agent-run final payloads into JSONL or chat.history can - // duplicate normal Pi assistant turns. The non-agent branch below has no Pi - // assistant turn, so it appends a gateway-injected assistant entry before + // duplicate normal embedded-agent assistant turns. The non-agent branch below has no + // runtime-owned assistant turn, so it appends a gateway-injected assistant entry before // broadcasting the final UI event. if (!agentRunStarted) { const btwReplies = deliveredReplies @@ -3249,7 +3249,7 @@ export const chatHandlers: GatewayRequestHandlers = { ...(ttsSupplementMarker ? { openclawTtsSupplement: ttsSupplementMarker } : {}), - // Keep this compatible with Pi stopReason enums even though this message isn't + // Keep this compatible with runner stopReason enums even though this message isn't // persisted to the transcript due to the append failure. stopReason: "stop", usage: { input: 0, output: 0, totalTokens: 0 }, diff --git a/src/gateway/server-methods/sessions.ts b/src/gateway/server-methods/sessions.ts index 5704a401e46..1bbcc7e2fb6 100644 --- a/src/gateway/server-methods/sessions.ts +++ b/src/gateway/server-methods/sessions.ts @@ -1,15 +1,15 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; -import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent"; import { resolveModelAgentRuntimeMetadata } from "../../agents/agent-runtime-metadata.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import { - abortEmbeddedPiRun, - isEmbeddedPiRunActive, - waitForEmbeddedPiRunEnd, -} from "../../agents/pi-embedded-runner/runs.js"; -import { compactEmbeddedPiSession } from "../../agents/pi-embedded.js"; + abortEmbeddedAgentRun, + isEmbeddedAgentRunActive, + waitForEmbeddedAgentRunEnd, +} from "../../agents/embedded-agent-runner/runs.js"; +import { compactEmbeddedAgentSession } from "../../agents/embedded-agent.js"; +import { CURRENT_SESSION_VERSION } from "../../agents/sessions/index.js"; import { clearSessionQueues } from "../../auto-reply/reply/queue/cleanup.js"; import { normalizeReasoningLevel, normalizeThinkLevel } from "../../auto-reply/thinking.js"; import { @@ -697,7 +697,7 @@ async function interruptSessionRunIfActive(params: { }); const hasEmbeddedRun = typeof params.sessionId === "string" && params.sessionId - ? isEmbeddedPiRunActive(params.sessionId) + ? isEmbeddedAgentRunActive(params.sessionId) : false; if (!hasTrackedRun && !hasEmbeddedRun) { @@ -737,13 +737,13 @@ async function interruptSessionRunIfActive(params: { } if (hasEmbeddedRun && params.sessionId) { - abortEmbeddedPiRun(params.sessionId); + abortEmbeddedAgentRun(params.sessionId); } clearSessionQueues([params.requestedKey, params.canonicalKey, params.sessionId]); if (hasEmbeddedRun && params.sessionId) { - const ended = await waitForEmbeddedPiRunEnd(params.sessionId, 15_000); + const ended = await waitForEmbeddedAgentRunEnd(params.sessionId, 15_000); if (!ended) { return { interrupted: true, @@ -2318,9 +2318,9 @@ export const sessionsHandlers: GatewayRequestHandlers = { phase: "start", sessionKey: target.canonicalKey, }); - let result: Awaited>; + let result: Awaited>; try { - result = await compactEmbeddedPiSession({ + result = await compactEmbeddedAgentSession({ sessionId, sessionKey: target.canonicalKey, allowGatewaySubagentBinding: true, diff --git a/src/gateway/server-reload-handlers.test.ts b/src/gateway/server-reload-handlers.test.ts index 2afcb4c708e..f468f54ddfe 100644 --- a/src/gateway/server-reload-handlers.test.ts +++ b/src/gateway/server-reload-handlers.test.ts @@ -87,7 +87,7 @@ vi.mock("../tasks/task-registry.maintenance.js", async () => { }; }); -vi.mock("../agents/pi-embedded-runner/run-state.js", () => ({ +vi.mock("../agents/embedded-agent-runner/run-state.js", () => ({ getActiveEmbeddedRunCount: () => hoisted.activeEmbeddedRunCount.value, listActiveEmbeddedRunSessionIds: () => hoisted.activeEmbeddedRunSessionIds, listActiveEmbeddedRunSessionKeys: () => hoisted.activeEmbeddedRunSessionKeys, diff --git a/src/gateway/server-reload-handlers.ts b/src/gateway/server-reload-handlers.ts index 36562c1f483..9ea9122b649 100644 --- a/src/gateway/server-reload-handlers.ts +++ b/src/gateway/server-reload-handlers.ts @@ -1,14 +1,14 @@ +import { disposeAllSessionMcpRuntimes } from "../agents/agent-bundle-mcp-tools.js"; +import { + getActiveEmbeddedRunCount, + listActiveEmbeddedRunSessionIds, + listActiveEmbeddedRunSessionKeys, +} from "../agents/embedded-agent-runner/run-state.js"; import { resetModelCatalogCache } from "../agents/model-catalog.js"; import { clearCurrentProviderAuthState, warmCurrentProviderAuthState, } from "../agents/model-provider-auth.js"; -import { disposeAllSessionMcpRuntimes } from "../agents/pi-bundle-mcp-tools.js"; -import { - getActiveEmbeddedRunCount, - listActiveEmbeddedRunSessionIds, - listActiveEmbeddedRunSessionKeys, -} from "../agents/pi-embedded-runner/run-state.js"; import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js"; import type { CliDeps } from "../cli/deps.types.js"; import { isRestartEnabled } from "../config/commands.flags.js"; diff --git a/src/gateway/server-startup-post-attach.test.ts b/src/gateway/server-startup-post-attach.test.ts index a8dfe0dac5b..529611c0e47 100644 --- a/src/gateway/server-startup-post-attach.test.ts +++ b/src/gateway/server-startup-post-attach.test.ts @@ -46,6 +46,8 @@ const hoisted = vi.hoisted(() => { allowed: true, inCatalog: true, })); + const resolveEmbeddedAgentRuntime = vi.fn(() => "openclaw"); + const ensureOpenClawModelsJson = vi.fn(async () => {}); const clearCurrentProviderAuthState = vi.fn(); const warmCurrentProviderAuthState = vi.fn(async (_cfg?: unknown, _options?: unknown) => {}); const setAuthProfileFailureHook = vi.fn(); @@ -78,6 +80,8 @@ const hoisted = vi.hoisted(() => { resolveHooksGmailModel, loadModelCatalog, getModelRefStatus, + resolveEmbeddedAgentRuntime, + ensureOpenClawModelsJson, clearCurrentProviderAuthState, warmCurrentProviderAuthState, setAuthProfileFailureHook, @@ -171,6 +175,14 @@ vi.mock("../agents/model-selection.js", () => ({ resolveHooksGmailModel: hoisted.resolveHooksGmailModel, })); +vi.mock("../agents/agent-runtime-id.js", () => ({ + resolveEmbeddedAgentRuntime: hoisted.resolveEmbeddedAgentRuntime, +})); + +vi.mock("../agents/models-config.js", () => ({ + ensureOpenClawModelsJson: hoisted.ensureOpenClawModelsJson, +})); + vi.mock("../agents/model-provider-auth.js", () => ({ clearCurrentProviderAuthState: hoisted.clearCurrentProviderAuthState, warmCurrentProviderAuthState: hoisted.warmCurrentProviderAuthState, @@ -284,6 +296,10 @@ describe("startGatewayPostAttachRuntime", () => { allowed: true, inCatalog: true, }); + hoisted.resolveEmbeddedAgentRuntime.mockReset(); + hoisted.resolveEmbeddedAgentRuntime.mockReturnValue("openclaw"); + hoisted.ensureOpenClawModelsJson.mockReset(); + hoisted.ensureOpenClawModelsJson.mockResolvedValue(undefined); hoisted.clearCurrentProviderAuthState.mockClear(); hoisted.warmCurrentProviderAuthState.mockReset(); hoisted.warmCurrentProviderAuthState.mockResolvedValue(undefined); diff --git a/src/gateway/server-startup-post-attach.ts b/src/gateway/server-startup-post-attach.ts index 0b28657d2c0..0964d018dfe 100644 --- a/src/gateway/server-startup-post-attach.ts +++ b/src/gateway/server-startup-post-attach.ts @@ -27,9 +27,12 @@ import type { startGatewayTailscaleExposure } from "./server-tailscale.js"; const ACP_BACKEND_READY_TIMEOUT_MS = 5_000; const ACP_BACKEND_READY_POLL_MS = 50; +const PRIMARY_MODEL_PREWARM_TIMEOUT_MS = 5_000; +const STARTUP_PROVIDER_DISCOVERY_TIMEOUT_MS = 5_000; const PROVIDER_AUTH_PREWARM_START_DELAY_MS = 1_000; const PROVIDER_AUTH_REWARM_DELAY_MS = 1_000; const DEFERRED_SIDECAR_START_DELAY_MS = 100; +const SKIP_STARTUP_MODEL_PREWARM_ENV = "OPENCLAW_SKIP_STARTUP_MODEL_PREWARM"; const QMD_STARTUP_IDLE_DELAY_MS = 120_000; const RESTART_SENTINEL_FILENAME = "restart-sentinel.json"; @@ -99,6 +102,11 @@ function shouldCheckRestartSentinel(env: NodeJS.ProcessEnv = process.env): boole return !env.VITEST && env.NODE_ENV !== "test"; } +function shouldSkipStartupModelPrewarm(env: NodeJS.ProcessEnv = process.env): boolean { + const raw = env[SKIP_STARTUP_MODEL_PREWARM_ENV]?.trim().toLowerCase(); + return raw === "1" || raw === "true" || raw === "yes" || raw === "on"; +} + function resolveGatewayMemoryStartupPolicy(cfg: OpenClawConfig): GatewayMemoryStartupPolicy { if (cfg.memory?.backend !== "qmd") { return { mode: "off" }; @@ -411,6 +419,21 @@ function hasGatewayStartHooks(pluginRegistry: ReturnType hook.hookName === "gateway_start"); } +function isConfiguredCliBackendPrimary(params: { + cfg: OpenClawConfig; + explicitPrimary: string; + normalizeProviderId: (provider: string) => string; +}): boolean { + const slashIndex = params.explicitPrimary.indexOf("/"); + if (slashIndex <= 0) { + return false; + } + const provider = params.normalizeProviderId(params.explicitPrimary.slice(0, slashIndex)); + return Object.keys(params.cfg.agents?.defaults?.cliBackends ?? {}).some( + (backend) => params.normalizeProviderId(backend) === provider, + ); +} + async function hasGatewayStartupInternalHookListeners(): Promise { const { hasInternalHookListeners } = await import("../hooks/internal-hooks.js"); return hasInternalHookListeners("gateway", "startup"); @@ -443,6 +466,121 @@ async function waitForAcpRuntimeBackendReady(params: { return false; } +async function prewarmConfiguredPrimaryModel(params: { + cfg: OpenClawConfig; + workspaceDir?: string; + log: { warn: (msg: string) => void }; +}): Promise { + const { resolveAgentModelPrimaryValue } = await import("../config/model-input.js"); + const explicitPrimary = resolveAgentModelPrimaryValue(params.cfg.agents?.defaults?.model)?.trim(); + if (!explicitPrimary) { + return; + } + const { normalizeProviderId } = await import("../agents/provider-id.js"); + if ( + isConfiguredCliBackendPrimary({ + cfg: params.cfg, + explicitPrimary, + normalizeProviderId, + }) + ) { + return; + } + const [ + { resolveAgentWorkspaceDir, resolveDefaultAgentDir, resolveDefaultAgentId }, + { DEFAULT_MODEL, DEFAULT_PROVIDER }, + { isCliProvider, resolveConfiguredModelRef }, + { resolveEmbeddedAgentRuntime }, + ] = await Promise.all([ + import("../agents/agent-scope.js"), + import("../agents/defaults.js"), + import("../agents/model-selection.js"), + import("../agents/agent-runtime-id.js"), + ]); + const { provider, model } = resolveConfiguredModelRef({ + cfg: params.cfg, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, + }); + if (isCliProvider(provider, params.cfg)) { + return; + } + const runtime = resolveEmbeddedAgentRuntime(); + if (runtime !== "auto" && runtime !== "openclaw") { + return; + } + // Keep startup prewarm metadata-only; resolving models can import provider runtimes and block readiness. + const { ensureOpenClawModelsJson } = await import("../agents/models-config.js"); + const agentDir = resolveDefaultAgentDir(params.cfg); + const workspaceDir = + params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg)); + try { + await ensureOpenClawModelsJson(params.cfg, agentDir, { + workspaceDir, + providerDiscoveryProviderIds: [provider], + providerDiscoveryTimeoutMs: STARTUP_PROVIDER_DISCOVERY_TIMEOUT_MS, + providerDiscoveryEntriesOnly: true, + }); + } catch (err) { + params.log.warn(`startup model warmup failed for ${provider}/${model}: ${String(err)}`); + } +} + +async function prewarmConfiguredPrimaryModelWithTimeout( + params: { + cfg: OpenClawConfig; + workspaceDir?: string; + log: { warn: (msg: string) => void }; + timeoutMs?: number; + }, + prewarm: typeof prewarmConfiguredPrimaryModel = prewarmConfiguredPrimaryModel, +): Promise { + let settled = false; + const warmup = prewarm(params) + .catch((err) => { + params.log.warn(`startup model warmup failed: ${String(err)}`); + }) + .finally(() => { + settled = true; + }); + const timeout = sleep(params.timeoutMs ?? PRIMARY_MODEL_PREWARM_TIMEOUT_MS, undefined, { + ref: false, + }).then(() => { + if (!settled) { + params.log.warn( + `startup model warmup timed out after ${params.timeoutMs ?? PRIMARY_MODEL_PREWARM_TIMEOUT_MS}ms; continuing without waiting`, + ); + } + }); + await Promise.race([warmup, timeout]); +} + +function schedulePrimaryModelPrewarm( + params: { + cfg: OpenClawConfig; + workspaceDir?: string; + log: { warn: (msg: string) => void }; + startupTrace?: GatewayStartupTrace; + }, + prewarm: typeof prewarmConfiguredPrimaryModel = prewarmConfiguredPrimaryModel, +): void { + if (shouldSkipStartupModelPrewarm()) { + return; + } + void measureStartup(params.startupTrace, "sidecars.model-prewarm", () => + prewarmConfiguredPrimaryModelWithTimeout( + { + cfg: params.cfg, + ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), + log: params.log, + }, + prewarm, + ), + ).catch((err) => { + params.log.warn(`startup model warmup failed: ${String(err)}`); + }); +} + export async function startGatewaySidecars(params: { cfg: OpenClawConfig; pluginRegistry: ReturnType; @@ -450,6 +588,7 @@ export async function startGatewaySidecars(params: { deps: CliDeps; startChannels: () => Promise; onChannelsStarted?: () => Awaitable; + prewarmPrimaryModel?: typeof prewarmConfiguredPrimaryModel; onPluginServices?: (pluginServices: PluginServicesHandle | null) => void; shouldStartPluginServices?: () => boolean; log: { warn: (msg: string) => void }; @@ -490,6 +629,15 @@ export async function startGatewaySidecars(params: { await measureStartup(params.startupTrace, "sidecars.channels", async () => { if (!skipChannels) { try { + schedulePrimaryModelPrewarm( + { + cfg: params.cfg, + workspaceDir: params.defaultWorkspaceDir, + log: params.log, + startupTrace: params.startupTrace, + }, + params.prewarmPrimaryModel, + ); await measureStartup(params.startupTrace, "sidecars.channel-start", () => params.startChannels(), ); @@ -1133,9 +1281,13 @@ export async function startGatewayPostAttachRuntime( export const testing = { hasRestartSentinelFileFast, + prewarmConfiguredPrimaryModel, + prewarmConfiguredPrimaryModelWithTimeout, refreshLatestUpdateRestartSentinelIfPresent, resolveGatewayMemoryStartupPolicy, scheduleProviderAuthStatePrewarm, + schedulePrimaryModelPrewarm, + shouldSkipStartupModelPrewarm, stopPostReadySidecarsAfterCloseStarted, }; export { testing as __testing }; diff --git a/src/gateway/server-startup.test.ts b/src/gateway/server-startup.test.ts new file mode 100644 index 00000000000..f7ffc29cd4b --- /dev/null +++ b/src/gateway/server-startup.test.ts @@ -0,0 +1,195 @@ +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; + +const ensureOpenClawModelsJsonMock = vi.fn< + ( + config: unknown, + agentDir: unknown, + options?: unknown, + ) => Promise<{ agentDir: string; wrote: boolean }> +>(async () => ({ agentDir: "/tmp/agent", wrote: false })); +const agentModelModuleLoadedMock = vi.fn(); +const resolveEmbeddedAgentRuntimeMock = vi.fn(() => "auto"); + +vi.mock("../agents/agent-scope.js", () => ({ + resolveDefaultAgentDir: () => "/tmp/agent", + resolveAgentWorkspaceDir: () => "/tmp/workspace", + resolveDefaultAgentId: () => "default", +})); + +vi.mock("../agents/models-config.js", () => ({ + ensureOpenClawModelsJson: (config: unknown, agentDir: unknown, options?: unknown) => + ensureOpenClawModelsJsonMock(config, agentDir, options), +})); + +vi.mock("../agents/embedded-agent-runner/model.js", () => { + agentModelModuleLoadedMock(); + return { + resolveModel: () => ({}), + }; +}); + +vi.mock("../agents/agent-runtime-id.js", () => ({ + resolveEmbeddedAgentRuntime: () => resolveEmbeddedAgentRuntimeMock(), +})); + +let prewarmConfiguredPrimaryModel: typeof import("./server-startup-post-attach.js").testing.prewarmConfiguredPrimaryModel; +let shouldSkipStartupModelPrewarm: typeof import("./server-startup-post-attach.js").testing.shouldSkipStartupModelPrewarm; + +function expectModelsJsonPrewarmCall(cfg: OpenClawConfig) { + expect(ensureOpenClawModelsJsonMock).toHaveBeenCalledTimes(1); + const [calledConfig, agentDir, options] = ensureOpenClawModelsJsonMock.mock.calls.at(0) ?? []; + expect(calledConfig).toBe(cfg); + expect(agentDir).toBe("/tmp/agent"); + expect(options).toEqual({ + workspaceDir: "/tmp/workspace", + providerDiscoveryProviderIds: ["openai-codex"], + providerDiscoveryTimeoutMs: 5000, + providerDiscoveryEntriesOnly: true, + }); +} + +describe("gateway startup primary model warmup", () => { + beforeAll(async () => { + ({ + testing: { prewarmConfiguredPrimaryModel, shouldSkipStartupModelPrewarm }, + } = await import("./server-startup-post-attach.js")); + }); + + beforeEach(() => { + ensureOpenClawModelsJsonMock.mockClear(); + agentModelModuleLoadedMock.mockClear(); + resolveEmbeddedAgentRuntimeMock.mockClear(); + resolveEmbeddedAgentRuntimeMock.mockReturnValue("auto"); + }); + + it("prewarms an explicit configured primary model", async () => { + const cfg = { + agents: { + defaults: { + model: { + primary: "openai-codex/gpt-5.4", + }, + }, + }, + } as OpenClawConfig; + + await prewarmConfiguredPrimaryModel({ + cfg, + log: { warn: vi.fn() }, + }); + + expectModelsJsonPrewarmCall(cfg); + expect(agentModelModuleLoadedMock).not.toHaveBeenCalled(); + }); + + it("skips warmup when no explicit primary model is configured", async () => { + await prewarmConfiguredPrimaryModel({ + cfg: {} as OpenClawConfig, + log: { warn: vi.fn() }, + }); + + expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled(); + expect(agentModelModuleLoadedMock).not.toHaveBeenCalled(); + }); + + it("honors the startup model prewarm skip env", () => { + expect(shouldSkipStartupModelPrewarm({})).toBe(false); + expect( + shouldSkipStartupModelPrewarm({ + OPENCLAW_SKIP_STARTUP_MODEL_PREWARM: "1", + }), + ).toBe(true); + expect( + shouldSkipStartupModelPrewarm({ + OPENCLAW_SKIP_STARTUP_MODEL_PREWARM: "true", + }), + ).toBe(true); + }); + + it("skips static warmup for configured CLI backends", async () => { + await prewarmConfiguredPrimaryModel({ + cfg: { + agents: { + defaults: { + model: { + primary: "codex-cli/gpt-5.5", + }, + cliBackends: { + "codex-cli": { + command: "codex", + args: ["exec"], + }, + }, + }, + }, + } as OpenClawConfig, + log: { warn: vi.fn() }, + }); + + expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled(); + expect(agentModelModuleLoadedMock).not.toHaveBeenCalled(); + }); + + it("skips static warmup when a non-OpenClaw agent runtime is forced", async () => { + resolveEmbeddedAgentRuntimeMock.mockReturnValue("codex"); + await prewarmConfiguredPrimaryModel({ + cfg: { + agents: { + defaults: { + model: { + primary: "codex/gpt-5.4", + }, + }, + }, + } as OpenClawConfig, + log: { warn: vi.fn() }, + }); + + expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled(); + expect(agentModelModuleLoadedMock).not.toHaveBeenCalled(); + }); + + it("keeps OpenClaw static warmup when the OpenClaw agent runtime is forced", async () => { + resolveEmbeddedAgentRuntimeMock.mockReturnValue("openclaw"); + const cfg = { + agents: { + defaults: { + model: { + primary: "openai-codex/gpt-5.4", + }, + }, + }, + } as OpenClawConfig; + + await prewarmConfiguredPrimaryModel({ + cfg, + log: { warn: vi.fn() }, + }); + + expectModelsJsonPrewarmCall(cfg); + expect(agentModelModuleLoadedMock).not.toHaveBeenCalled(); + }); + + it("warns when scoped models.json preparation fails", async () => { + ensureOpenClawModelsJsonMock.mockRejectedValueOnce(new Error("models write failed")); + const warn = vi.fn(); + + await prewarmConfiguredPrimaryModel({ + cfg: { + agents: { + defaults: { + model: { + primary: "codex/gpt-5.4", + }, + }, + }, + } as OpenClawConfig, + log: { warn }, + }); + + expect(warn).toHaveBeenCalledWith( + "startup model warmup failed for codex/gpt-5.4: Error: models write failed", + ); + }); +}); diff --git a/src/gateway/server.agent.gateway-server-agent-a.test.ts b/src/gateway/server.agent.gateway-server-agent-a.test.ts index 6407055e3ad..549441f9518 100644 --- a/src/gateway/server.agent.gateway-server-agent-a.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-a.test.ts @@ -12,7 +12,7 @@ import { agentCommand, connectOk, installGatewayTestHooks, - piSdkMock, + agentDiscoveryMock, rpcReq, startServerWithClient, testState, @@ -451,8 +451,8 @@ describe("gateway server agent", () => { { id: "vision", model: "ollama-cloud/gemma4:31b" }, ], }; - piSdkMock.enabled = true; - piSdkMock.models = [ + agentDiscoveryMock.enabled = true; + agentDiscoveryMock.models = [ { id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index 28f8f3f3cdf..3af02f56010 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -1,5 +1,5 @@ import { monitorEventLoopDelay, performance } from "node:perf_hooks"; -import { getActiveEmbeddedRunCount } from "../agents/pi-embedded-runner/run-state.js"; +import { getActiveEmbeddedRunCount } from "../agents/embedded-agent-runner/run-state.js"; import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js"; import type { ChannelRuntimeSurface } from "../channels/plugins/channel-runtime-surface.types.js"; import { diff --git a/src/gateway/server.models-voicewake-misc.test.ts b/src/gateway/server.models-voicewake-misc.test.ts index faac61afbf3..fff0b447a75 100644 --- a/src/gateway/server.models-voicewake-misc.test.ts +++ b/src/gateway/server.models-voicewake-misc.test.ts @@ -16,7 +16,7 @@ import { getFreePort, installGatewayTestHooks, onceMessage, - piSdkMock, + agentDiscoveryMock, rpcReq, resetTestPluginRegistry, setTestPluginRegistry, @@ -90,14 +90,14 @@ type ModelCatalogRpcEntry = { reasoning?: boolean; }; -type PiCatalogFixtureEntry = { +type AgentCatalogFixtureEntry = { id: string; provider: string; name?: string; contextWindow?: number; }; -const buildPiCatalogFixture = (): PiCatalogFixtureEntry[] => [ +const buildAgentCatalogFixture = (): AgentCatalogFixtureEntry[] => [ { id: "gpt-test-z", provider: "openai", contextWindow: 0 }, { id: "gpt-test-a", @@ -153,14 +153,14 @@ describe("gateway server models + voicewake", () => { : await rpcReq<{ models: ModelCatalogRpcEntry[] }>(ws, "models.list"), ); - const setPiCatalog = async (entries: PiCatalogFixtureEntry[]) => { - piSdkMock.enabled = true; - piSdkMock.models = entries; + const setAgentCatalog = async (entries: AgentCatalogFixtureEntry[]) => { + agentDiscoveryMock.enabled = true; + agentDiscoveryMock.models = entries; await resetGatewayModelCatalogCacheForTest(); }; - const seedPiCatalog = async () => { - await setPiCatalog(buildPiCatalogFixture()); + const seedAgentModelCatalog = async () => { + await setAgentCatalog(buildAgentCatalogFixture()); }; const withModelsConfig = async (config: unknown, run: () => Promise): Promise => { @@ -219,7 +219,7 @@ describe("gateway server models + voicewake", () => { }, }, async () => { - await seedPiCatalog(); + await seedAgentModelCatalog(); const res = await listModels(); expect(res.ok).toBe(true); expect(res.payload?.models).toEqual(options.expected); @@ -471,7 +471,7 @@ describe("gateway server models + voicewake", () => { }); test("models.list all view returns model catalog", async () => { - await seedPiCatalog(); + await seedAgentModelCatalog(); const res1 = await listModels({ view: "all" }); const res2 = await listModels({ view: "all" }); @@ -482,7 +482,7 @@ describe("gateway server models + voicewake", () => { const models = res1.payload?.models ?? []; expect(models).toEqual(expectedSortedCatalog()); - expect(piSdkMock.discoverCalls).toBe(1); + expect(agentDiscoveryMock.discoverCalls).toBe(1); }); test("models.list default view uses configured providers instead of the full catalog", async () => { @@ -498,7 +498,7 @@ describe("gateway server models + voicewake", () => { }, }, async () => { - await setPiCatalog([ + await setAgentCatalog([ { id: "remote-a", provider: "unauth-a", name: "Remote A" }, { id: "remote-b", provider: "unauth-b", name: "Remote B" }, ]); @@ -522,12 +522,12 @@ describe("gateway server models + voicewake", () => { }, async () => { await withModelsConfig({}, async () => { - await seedPiCatalog(); - const discoverCallsBefore = piSdkMock.discoverCalls; + await seedAgentModelCatalog(); + const discoverCallsBefore = agentDiscoveryMock.discoverCalls; const res = await listModels({ view: "configured" }); expect(res.ok).toBe(true); expect(res.payload?.models).toStrictEqual([]); - expect(piSdkMock.discoverCalls).toBe(discoverCallsBefore); + expect(agentDiscoveryMock.discoverCalls).toBe(discoverCallsBefore); }); }, ); @@ -550,7 +550,7 @@ describe("gateway server models + voicewake", () => { }, }, async () => { - await setPiCatalog([ + await setAgentCatalog([ { id: "remote-a", provider: "unauth-a", name: "Remote A" }, { id: "remote-b", provider: "unauth-b", name: "Remote B" }, ]); @@ -590,7 +590,7 @@ describe("gateway server models + voicewake", () => { }, }, async () => { - await seedPiCatalog(); + await seedAgentModelCatalog(); const res = await listModels({ view: "configured" }); expect(res.ok).toBe(true); expect(res.payload?.models).toEqual([ @@ -617,7 +617,7 @@ describe("gateway server models + voicewake", () => { }, }, async () => { - await seedPiCatalog(); + await seedAgentModelCatalog(); const res = await listModels({ view: "all" }); expect(res.ok).toBe(true); expect(res.payload?.models).toEqual(expectedSortedCatalog()); @@ -690,7 +690,7 @@ describe("gateway server models + voicewake", () => { }, }, async () => { - await seedPiCatalog(); + await seedAgentModelCatalog(); const res = await listModels(); expect(res.ok).toBe(true); const models = res.payload?.models ?? []; @@ -731,7 +731,7 @@ describe("gateway server models + voicewake", () => { }, }, async () => { - await seedPiCatalog(); + await seedAgentModelCatalog(); const res = await listModels(); expect(res.ok).toBe(true); const models = res.payload?.models ?? []; @@ -746,8 +746,8 @@ describe("gateway server models + voicewake", () => { }); test("models.list rejects unknown params", async () => { - piSdkMock.enabled = true; - piSdkMock.models = [{ id: "gpt-test-a", name: "A", provider: "openai" }]; + agentDiscoveryMock.enabled = true; + agentDiscoveryMock.models = [{ id: "gpt-test-a", name: "A", provider: "openai" }]; const res = await rpcReq(ws, "models.list", { extra: true }); expect(res.ok).toBe(false); diff --git a/src/gateway/server.sessions.compaction.test.ts b/src/gateway/server.sessions.compaction.test.ts index c363f78d171..714734a8e85 100644 --- a/src/gateway/server.sessions.compaction.test.ts +++ b/src/gateway/server.sessions.compaction.test.ts @@ -6,7 +6,7 @@ import { withEnvAsync } from "../test-utils/env.js"; import { embeddedRunMock, onceMessage, - piSdkMock, + agentDiscoveryMock, rpcReq, startConnectedServerWithClient, writeSessionStore, @@ -367,8 +367,8 @@ test("sessions.compact without maxLines runs embedded manual compaction for chec expect(endPayload.operationId).toBe(startPayload.operationId); expect(typeof startPayload.ts).toBe("number"); expect(typeof endPayload.ts).toBe("number"); - expect(embeddedRunMock.compactEmbeddedPiSession).toHaveBeenCalledTimes(1); - const compactionCall = embeddedRunMock.compactEmbeddedPiSession.mock.calls.at(0)?.[0] as + expect(embeddedRunMock.compactEmbeddedAgentSession).toHaveBeenCalledTimes(1); + const compactionCall = embeddedRunMock.compactEmbeddedAgentSession.mock.calls.at(0)?.[0] as | { agentHarnessId?: string; allowGatewaySubagentBinding?: boolean; @@ -548,8 +548,8 @@ test("sessions.patch preserves nested model ids under provider overrides", async const started = await startConnectedServerWithClient(); const { server, ws } = started; try { - piSdkMock.enabled = true; - piSdkMock.models = [ + agentDiscoveryMock.enabled = true; + agentDiscoveryMock.models = [ { id: "moonshotai/kimi-k2.5", name: "Kimi K2.5 (NVIDIA)", provider: "nvidia" }, ]; diff --git a/src/gateway/server.sessions.create.test.ts b/src/gateway/server.sessions.create.test.ts index c59938f3e26..b6bf2c48348 100644 --- a/src/gateway/server.sessions.create.test.ts +++ b/src/gateway/server.sessions.create.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { expect, test } from "vitest"; -import { piSdkMock, rpcReq, testState, writeSessionStore } from "./test-helpers.js"; +import { agentDiscoveryMock, rpcReq, testState, writeSessionStore } from "./test-helpers.js"; import { setupGatewaySessionsTestHarness, sessionStoreEntry, @@ -19,8 +19,8 @@ function requireNonEmptyString(value: string | undefined, label: string): string test("sessions.create stores dashboard session model and parent linkage, and creates a transcript", async () => { const { dir, storePath } = await createSessionStoreDir(); - piSdkMock.enabled = true; - piSdkMock.models = [{ id: "gpt-test-a", name: "A", provider: "openai" }]; + agentDiscoveryMock.enabled = true; + agentDiscoveryMock.models = [{ id: "gpt-test-a", name: "A", provider: "openai" }]; await writeSessionStore({ entries: { main: sessionStoreEntry("sess-parent"), diff --git a/src/gateway/server.sessions.store-rpc.test.ts b/src/gateway/server.sessions.store-rpc.test.ts index 8a74e41a0ef..7d7e3a5a915 100644 --- a/src/gateway/server.sessions.store-rpc.test.ts +++ b/src/gateway/server.sessions.store-rpc.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { expect, test, vi } from "vitest"; -import { piSdkMock, rpcReq, testState, writeSessionStore } from "./test-helpers.js"; +import { agentDiscoveryMock, rpcReq, testState, writeSessionStore } from "./test-helpers.js"; import { directSessionReq as directSessionHandlerReq, setupGatewaySessionsTestHarness, @@ -99,7 +99,7 @@ test("lists and patches session store via sessions.* RPC", async () => { broadcastToConnIds: vi.fn(), getSessionEventSubscriberConnIds: () => new Set(), logGateway: { debug: vi.fn() }, - loadGatewayModelCatalog: async () => piSdkMock.models, + loadGatewayModelCatalog: async () => agentDiscoveryMock.models, getRuntimeConfig: getRuntimeConfig, } as never; async function directSessionReq( @@ -343,8 +343,8 @@ test("lists and patches session store via sessions.* RPC", async () => { "agent:main:subagent:one", ); - piSdkMock.enabled = true; - piSdkMock.models = [{ id: "gpt-test-a", name: "A", provider: "openai" }]; + agentDiscoveryMock.enabled = true; + agentDiscoveryMock.models = [{ id: "gpt-test-a", name: "A", provider: "openai" }]; const modelPatched = await directSessionReq<{ ok: true; entry: { diff --git a/src/gateway/session-compaction-checkpoints.test.ts b/src/gateway/session-compaction-checkpoints.test.ts index fb00ad07058..a90ac5181c5 100644 --- a/src/gateway/session-compaction-checkpoints.test.ts +++ b/src/gateway/session-compaction-checkpoints.test.ts @@ -2,8 +2,8 @@ import fsSync from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AssistantMessage } from "@earendil-works/pi-ai"; -import { CURRENT_SESSION_VERSION, SessionManager } from "@earendil-works/pi-coding-agent"; +import { CURRENT_SESSION_VERSION, SessionManager } from "openclaw/plugin-sdk/agent-sessions"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { afterEach, describe, expect, test, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { diff --git a/src/gateway/session-compaction-checkpoints.ts b/src/gateway/session-compaction-checkpoints.ts index 5fbcd893307..2e7137dd6d0 100644 --- a/src/gateway/session-compaction-checkpoints.ts +++ b/src/gateway/session-compaction-checkpoints.ts @@ -5,8 +5,8 @@ import { CURRENT_SESSION_VERSION, migrateSessionEntries, SessionManager, - type FileEntry as PiSessionFileEntry, -} from "@earendil-works/pi-coding-agent"; + type FileEntry as SessionFileEntry, +} from "../agents/sessions/index.js"; import { updateSessionStore } from "../config/sessions.js"; import type { SessionCompactionCheckpoint, @@ -214,8 +214,8 @@ function parseTranscriptLineId( async function readTranscriptEntriesForForkAsync(params: { sessionFile: string; stopAfterEntryId?: string; -}): Promise { - const entries: PiSessionFileEntry[] = []; +}): Promise { + const entries: SessionFileEntry[] = []; const stopAfterEntryId = params.stopAfterEntryId?.trim(); let foundStopEntry = false; try { @@ -225,7 +225,7 @@ async function readTranscriptEntriesForForkAsync(params: { if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { continue; } - entries.push(parsed as PiSessionFileEntry); + entries.push(parsed as SessionFileEntry); if ( stopAfterEntryId && (parsed as { type?: unknown; id?: unknown }).type !== "session" && @@ -235,7 +235,7 @@ async function readTranscriptEntriesForForkAsync(params: { break; } } catch { - // Match pi-coding-agent's loader: malformed JSONL entries are ignored. + // Match session runtime's loader: malformed JSONL entries are ignored. } } } catch { @@ -252,9 +252,9 @@ async function readTranscriptEntriesForForkAsync(params: { } function trimTranscriptEntriesThroughLeaf( - entries: PiSessionFileEntry[], + entries: SessionFileEntry[], leafId: string | undefined, -): PiSessionFileEntry[] | null { +): SessionFileEntry[] | null { const normalizedLeafId = leafId?.trim(); if (!normalizedLeafId) { return entries; diff --git a/src/gateway/session-reset-service.ts b/src/gateway/session-reset-service.ts index 1d57b5c8b38..69c9ef77e38 100644 --- a/src/gateway/session-reset-service.ts +++ b/src/gateway/session-reset-service.ts @@ -1,15 +1,15 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import path from "node:path"; -import { CURRENT_SESSION_VERSION } from "@earendil-works/pi-coding-agent"; import { getAcpSessionManager } from "../acp/control-plane/manager.js"; import { getAcpRuntimeBackend } from "../acp/runtime/registry.js"; import { readAcpSessionEntry, upsertAcpSessionMeta } from "../acp/runtime/session-meta.js"; +import { retireSessionMcpRuntime } from "../agents/agent-bundle-mcp-tools.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { clearBootstrapSnapshot } from "../agents/bootstrap-cache.js"; import { clearAllCliSessions } from "../agents/cli-session.js"; -import { retireSessionMcpRuntime } from "../agents/pi-bundle-mcp-tools.js"; -import { abortEmbeddedPiRun, waitForEmbeddedPiRunEnd } from "../agents/pi-embedded.js"; +import { abortEmbeddedAgentRun, waitForEmbeddedAgentRunEnd } from "../agents/embedded-agent.js"; +import { CURRENT_SESSION_VERSION } from "../agents/sessions/index.js"; import { stopSubagentsForRequester } from "../auto-reply/reply/abort.js"; import { buildSessionEndHookPayload, @@ -388,8 +388,8 @@ async function ensureSessionRuntimeCleanup(params: { await closeTrackedBrowserTabs(); return undefined; } - abortEmbeddedPiRun(params.sessionId); - const ended = await waitForEmbeddedPiRunEnd(params.sessionId, 15_000); + abortEmbeddedAgentRun(params.sessionId); + const ended = await waitForEmbeddedAgentRunEnd(params.sessionId, 15_000); clearBootstrapSnapshot(params.target.canonicalKey); if (ended) { await retireSessionMcpRuntime({ diff --git a/src/gateway/session-utils.fs.test.ts b/src/gateway/session-utils.fs.test.ts index ae2b3b62b62..16e7071b27f 100644 --- a/src/gateway/session-utils.fs.test.ts +++ b/src/gateway/session-utils.fs.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { SessionManager } from "@earendil-works/pi-coding-agent"; +import { SessionManager } from "openclaw/plugin-sdk/agent-sessions"; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from "vitest"; import { estimateStringChars, estimateTokensFromChars } from "../utils/cjk-chars.js"; import { createToolSummaryPreviewTranscriptLines } from "./session-preview.test-helpers.js"; diff --git a/src/gateway/sessions-history-http.test.ts b/src/gateway/sessions-history-http.test.ts index 336cf7f64be..def5197ce64 100644 --- a/src/gateway/sessions-history-http.test.ts +++ b/src/gateway/sessions-history-http.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AssistantMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { afterEach, describe, expect, test } from "vitest"; import { appendAssistantMessageToSessionTranscript, diff --git a/src/gateway/test-helpers.mocks.ts b/src/gateway/test-helpers.mocks.ts index 855c9db8048..8c90cc48944 100644 --- a/src/gateway/test-helpers.mocks.ts +++ b/src/gateway/test-helpers.mocks.ts @@ -14,7 +14,7 @@ import { getReplyFromConfig, getGatewayTestHoistedState, mockGetReplyFromConfigOnce, - piSdkMock, + agentDiscoveryMock, runBtwSideQuestion, sendWhatsAppMock, sessionStoreSaveDelayMs, @@ -34,7 +34,7 @@ export { embeddedRunMock, getReplyFromConfig, mockGetReplyFromConfigOnce, - piSdkMock, + agentDiscoveryMock, runBtwSideQuestion, sendWhatsAppMock, sessionStoreSaveDelayMs, @@ -49,14 +49,14 @@ const gatewayTestHoisted = getGatewayTestHoistedState(); function createEmbeddedRunMockExports() { return { - compactEmbeddedPiSession: (...args: unknown[]) => - embeddedRunMock.compactEmbeddedPiSession(...args), - isEmbeddedPiRunActive: (sessionId: string) => embeddedRunMock.activeIds.has(sessionId), - abortEmbeddedPiRun: (sessionId: string) => { + compactEmbeddedAgentSession: (...args: unknown[]) => + embeddedRunMock.compactEmbeddedAgentSession(...args), + isEmbeddedAgentRunActive: (sessionId: string) => embeddedRunMock.activeIds.has(sessionId), + abortEmbeddedAgentRun: (sessionId: string) => { embeddedRunMock.abortCalls.push(sessionId); return embeddedRunMock.activeIds.has(sessionId); }, - waitForEmbeddedPiRunEnd: async (sessionId: string) => { + waitForEmbeddedAgentRunEnd: async (sessionId: string) => { embeddedRunMock.waitCalls.push(sessionId); return embeddedRunMock.waitResults.get(sessionId) ?? true; }, @@ -93,9 +93,9 @@ function createDispatchInboundMessageMockExports( }; } -vi.mock("../agents/pi-model-discovery.js", async () => { - const actual = await vi.importActual( - "../agents/pi-model-discovery.js", +vi.mock("../agents/agent-model-discovery.js", async () => { + const actual = await vi.importActual( + "../agents/agent-model-discovery.js", ); const createActualRegistry = (...args: Parameters) => { @@ -128,31 +128,31 @@ vi.mock("../agents/pi-model-discovery.js", async () => { private readonly actualRegistry?: ReturnType; constructor(authStorage: unknown, modelsFile: string) { - if (!piSdkMock.enabled) { + if (!agentDiscoveryMock.enabled) { this.actualRegistry = createActualRegistry(authStorage as never, path.dirname(modelsFile)); } } getAll() { - if (!piSdkMock.enabled) { + if (!agentDiscoveryMock.enabled) { return this.actualRegistry?.getAll() ?? []; } - piSdkMock.discoverCalls += 1; - return piSdkMock.models as Array<{ provider?: string; id?: string }>; + agentDiscoveryMock.discoverCalls += 1; + return agentDiscoveryMock.models as Array<{ provider?: string; id?: string }>; } getAvailable() { - if (!piSdkMock.enabled) { + if (!agentDiscoveryMock.enabled) { return this.actualRegistry?.getAvailable() ?? []; } - return piSdkMock.models as Array<{ provider?: string; id?: string }>; + return agentDiscoveryMock.models as Array<{ provider?: string; id?: string }>; } find(provider: string, modelId: string) { - if (!piSdkMock.enabled) { + if (!agentDiscoveryMock.enabled) { return this.actualRegistry?.find(provider, modelId); } - return (piSdkMock.models as Array<{ provider?: string; id?: string }>).find( + return (agentDiscoveryMock.models as Array<{ provider?: string; id?: string }>).find( (model) => model.provider === provider && model.id === modelId, ); } @@ -160,6 +160,8 @@ vi.mock("../agents/pi-model-discovery.js", async () => { return { ...actual, + discoverModels: (authStorage: Parameters[0], agentDir: string) => + new MockModelRegistry(authStorage, path.join(agentDir, "models.json")), ModelRegistry: MockModelRegistry, }; }); @@ -229,30 +231,28 @@ vi.mock("../config/io.js", async () => { }; }); -vi.mock("../agents/pi-embedded.js", async () => { - return await importEmbeddedRunMockModule( - "../agents/pi-embedded.js", +vi.mock("../agents/embedded-agent.js", async () => { + return await importEmbeddedRunMockModule( + "../agents/embedded-agent.js", ); }); -vi.mock("/src/agents/pi-embedded.js", async () => { - return await importEmbeddedRunMockModule( - "../agents/pi-embedded.js", +vi.mock("/src/agents/embedded-agent.js", async () => { + return await importEmbeddedRunMockModule( + "../agents/embedded-agent.js", ); }); -vi.mock("../agents/pi-embedded-runner/runs.js", async () => { - return await importEmbeddedRunMockModule( - "../agents/pi-embedded-runner/runs.js", - { includeActiveCount: true }, - ); +vi.mock("../agents/embedded-agent-runner/runs.js", async () => { + return await importEmbeddedRunMockModule< + typeof import("../agents/embedded-agent-runner/runs.js") + >("../agents/embedded-agent-runner/runs.js", { includeActiveCount: true }); }); -vi.mock("/src/agents/pi-embedded-runner/runs.js", async () => { - return await importEmbeddedRunMockModule( - "../agents/pi-embedded-runner/runs.js", - { includeActiveCount: true }, - ); +vi.mock("/src/agents/embedded-agent-runner/runs.js", async () => { + return await importEmbeddedRunMockModule< + typeof import("../agents/embedded-agent-runner/runs.js") + >("../agents/embedded-agent-runner/runs.js", { includeActiveCount: true }); }); vi.mock("../commands/health.js", () => ({ diff --git a/src/gateway/test-helpers.runtime-state.ts b/src/gateway/test-helpers.runtime-state.ts index 1f6f9194840..f7265483632 100644 --- a/src/gateway/test-helpers.runtime-state.ts +++ b/src/gateway/test-helpers.runtime-state.ts @@ -23,13 +23,13 @@ type AgentCommandFn = (...args: unknown[]) => Promise; type SendWhatsAppFn = (...args: unknown[]) => Promise<{ messageId: string; toJid: string }>; export type RunBtwSideQuestionFn = (...args: unknown[]) => Promise; type DispatchInboundMessageFn = (...args: unknown[]) => Promise; -type CompactEmbeddedPiSessionFn = (...args: unknown[]) => Promise; +type CompactEmbeddedAgentSessionFn = (...args: unknown[]) => Promise; const GATEWAY_TEST_CONFIG_ROOT_KEY = Symbol.for("openclaw.gatewayTestHelpers.configRoot"); type GatewayTestHoistedState = { testTailnetIPv4: { value: string | undefined }; - piSdkMock: { + agentDiscoveryMock: { enabled: boolean; discoverCalls: number; models: Array<{ @@ -52,7 +52,7 @@ type GatewayTestHoistedState = { abortCalls: string[]; waitCalls: string[]; waitResults: Map; - compactEmbeddedPiSession: Mock; + compactEmbeddedAgentSession: Mock; }; testTailscaleWhois: { value: TailscaleWhoisIdentity | null }; getReplyFromConfig: Mock; @@ -86,7 +86,7 @@ const gatewayTestHoisted = vi.hoisted(() => { } const created: GatewayTestHoistedState = { testTailnetIPv4: { value: undefined }, - piSdkMock: { + agentDiscoveryMock: { enabled: false, discoverCalls: 0, models: [], @@ -102,7 +102,7 @@ const gatewayTestHoisted = vi.hoisted(() => { abortCalls: [], waitCalls: [], waitResults: new Map(), - compactEmbeddedPiSession: vi.fn().mockResolvedValue({ + compactEmbeddedAgentSession: vi.fn().mockResolvedValue({ ok: true, compacted: true, result: { @@ -146,7 +146,7 @@ export function getGatewayTestHoistedState(): GatewayTestHoistedState { export const testTailnetIPv4 = gatewayTestHoisted.testTailnetIPv4; export const testTailscaleWhois = gatewayTestHoisted.testTailscaleWhois; -export const piSdkMock = gatewayTestHoisted.piSdkMock; +export const agentDiscoveryMock = gatewayTestHoisted.agentDiscoveryMock; export const cronIsolatedRun = gatewayTestHoisted.cronIsolatedRun; export const agentCommand = gatewayTestHoisted.agentCommand; export const runBtwSideQuestion = gatewayTestHoisted.runBtwSideQuestion; diff --git a/src/gateway/test-helpers.server.ts b/src/gateway/test-helpers.server.ts index bf432ee4d3e..45668191c4d 100644 --- a/src/gateway/test-helpers.server.ts +++ b/src/gateway/test-helpers.server.ts @@ -47,7 +47,7 @@ import { cronIsolatedRun, embeddedRunMock, getReplyFromConfig, - piSdkMock, + agentDiscoveryMock, sendWhatsAppMock, sessionStoreSaveDelayMs, setTestConfigRoot, @@ -72,7 +72,6 @@ const GATEWAY_TEST_ENV_KEYS = [ "OPENCLAW_STATE_DIR", "OPENCLAW_CONFIG_PATH", "OPENCLAW_AGENT_DIR", - "PI_CODING_AGENT_DIR", "OPENCLAW_GATEWAY_TOKEN", "OPENCLAW_SKIP_BROWSER_CONTROL_SERVER", "OPENCLAW_SKIP_GMAIL_WATCHER", @@ -235,7 +234,6 @@ async function setupGatewayTestHome() { process.env.OPENCLAW_STATE_DIR = path.join(tempHome, ".openclaw"); delete process.env.OPENCLAW_CONFIG_PATH; delete process.env.OPENCLAW_AGENT_DIR; - delete process.env.PI_CODING_AGENT_DIR; } function applyGatewaySkipEnv() { @@ -347,8 +345,8 @@ async function resetGatewayTestState(options: { uniqueConfigRoot: boolean }) { embeddedRunMock.abortCalls = []; embeddedRunMock.waitCalls = []; embeddedRunMock.waitResults.clear(); - embeddedRunMock.compactEmbeddedPiSession.mockReset(); - embeddedRunMock.compactEmbeddedPiSession.mockResolvedValue({ + embeddedRunMock.compactEmbeddedAgentSession.mockReset(); + embeddedRunMock.compactEmbeddedAgentSession.mockResolvedValue({ ok: true, compacted: true, result: { @@ -364,9 +362,9 @@ async function resetGatewayTestState(options: { uniqueConfigRoot: boolean }) { resetAgentRunContextForTest(); const mod = await getServerModule(); await mod.resetModelCatalogCacheForTest(); - piSdkMock.enabled = false; - piSdkMock.discoverCalls = 0; - piSdkMock.models = []; + agentDiscoveryMock.enabled = false; + agentDiscoveryMock.discoverCalls = 0; + agentDiscoveryMock.models = []; } async function cleanupGatewayTestHome(options: { restoreEnv: boolean }) { @@ -438,8 +436,8 @@ async function resetGatewayTestRuntimeOnly() { embeddedRunMock.abortCalls = []; embeddedRunMock.waitCalls = []; embeddedRunMock.waitResults.clear(); - embeddedRunMock.compactEmbeddedPiSession.mockReset(); - embeddedRunMock.compactEmbeddedPiSession.mockResolvedValue({ + embeddedRunMock.compactEmbeddedAgentSession.mockReset(); + embeddedRunMock.compactEmbeddedAgentSession.mockResolvedValue({ ok: true, compacted: true, result: { diff --git a/src/gateway/test-helpers.ts b/src/gateway/test-helpers.ts index 0e9d7d1fdbb..7cc0d3ac022 100644 --- a/src/gateway/test-helpers.ts +++ b/src/gateway/test-helpers.ts @@ -5,7 +5,7 @@ export { embeddedRunMock, getReplyFromConfig, mockGetReplyFromConfigOnce, - piSdkMock, + agentDiscoveryMock, testState, testTailnetIPv4, testTailscaleWhois, diff --git a/src/gateway/test/server-sessions.test-helpers.ts b/src/gateway/test/server-sessions.test-helpers.ts index 2c066841ca9..259d3e2edeb 100644 --- a/src/gateway/test/server-sessions.test-helpers.ts +++ b/src/gateway/test/server-sessions.test-helpers.ts @@ -2,7 +2,7 @@ import fsSync from "node:fs"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import type { AssistantMessage, UserMessage } from "@earendil-works/pi-ai"; +import type { AssistantMessage, UserMessage } from "openclaw/plugin-sdk/llm"; import { afterAll, beforeAll, beforeEach, expect, vi } from "vitest"; import type { SessionEntry } from "../../config/sessions.js"; import type { InternalHookEvent } from "../../hooks/internal-hooks.js"; @@ -12,19 +12,19 @@ import { connectOk, embeddedRunMock, installGatewayTestHooks, - piSdkMock, + agentDiscoveryMock, rpcReq, testState, writeSessionStore, } from "../test-helpers.js"; let sessionManagerModulePromise: - | Promise + | Promise | undefined; let gatewayConfigModulePromise: Promise | undefined; export async function getSessionManagerModule() { - sessionManagerModulePromise ??= import("@earendil-works/pi-coding-agent"); + sessionManagerModulePromise ??= import("../../agents/sessions/index.js"); return await sessionManagerModulePromise; } @@ -231,7 +231,7 @@ vi.mock("../../plugin-sdk/browser-maintenance.js", () => ({ movePathToTrash: vi.fn(async () => {}), })); -vi.mock("../../agents/pi-bundle-mcp-tools.js", () => ({ +vi.mock("../../agents/agent-bundle-mcp-tools.js", () => ({ disposeSessionMcpRuntime: bundleMcpRuntimeMocks.disposeSessionMcpRuntime, disposeAllSessionMcpRuntimes: bundleMcpRuntimeMocks.disposeAllSessionMcpRuntimes, retireSessionMcpRuntime: ({ sessionId }: { sessionId?: string | null }) => @@ -495,7 +495,7 @@ export async function directSessionReq( context: { broadcastToConnIds: vi.fn(), getSessionEventSubscriberConnIds: () => new Set(), - loadGatewayModelCatalog: async () => piSdkMock.models, + loadGatewayModelCatalog: async () => agentDiscoveryMock.models, getRuntimeConfig: getRuntimeConfig, ...opts?.context, } as never, diff --git a/src/gateway/tool-resolution.ts b/src/gateway/tool-resolution.ts index af29d7f527f..f4903a25d43 100644 --- a/src/gateway/tool-resolution.ts +++ b/src/gateway/tool-resolution.ts @@ -1,11 +1,11 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; -import { createOpenClawTools } from "../agents/openclaw-tools.js"; import { resolveEffectiveToolPolicy, resolveGroupToolPolicy, resolveInheritedToolPolicyForSession, resolveSubagentToolPolicyForSession, -} from "../agents/pi-tools.policy.js"; +} from "../agents/agent-tools.policy.js"; +import { createOpenClawTools } from "../agents/openclaw-tools.js"; import { isSubagentEnvelopeSession, resolveSubagentCapabilityStore, diff --git a/src/gateway/tools-invoke-http.cron-regression.test.ts b/src/gateway/tools-invoke-http.cron-regression.test.ts index 9c7ceb15b6e..fe1a8cf51f4 100644 --- a/src/gateway/tools-invoke-http.cron-regression.test.ts +++ b/src/gateway/tools-invoke-http.cron-regression.test.ts @@ -35,11 +35,11 @@ vi.mock("../logger.js", () => ({ logWarn: noWarnLog, })); -vi.mock("../agents/pi-tools.js", () => ({ +vi.mock("../agents/agent-tools.js", () => ({ resolveToolLoopDetectionConfig, })); -vi.mock("../agents/pi-tools.before-tool-call.js", () => ({ +vi.mock("../agents/agent-tools.before-tool-call.js", () => ({ runBeforeToolCallHook, })); diff --git a/src/gateway/tools-invoke-http.test.ts b/src/gateway/tools-invoke-http.test.ts index 253d052247c..675b176384b 100644 --- a/src/gateway/tools-invoke-http.test.ts +++ b/src/gateway/tools-invoke-http.test.ts @@ -1,7 +1,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import type { AddressInfo } from "node:net"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import type { runBeforeToolCallHook as runBeforeToolCallHookType } from "../agents/pi-tools.before-tool-call.js"; +import type { runBeforeToolCallHook as runBeforeToolCallHookType } from "../agents/agent-tools.before-tool-call.js"; type RunBeforeToolCallHook = typeof runBeforeToolCallHookType; type RunBeforeToolCallHookArgs = Parameters[0]; @@ -203,11 +203,11 @@ vi.mock("../agents/openclaw-tools.js", () => { }; }); -vi.mock("../agents/pi-tools.js", () => ({ +vi.mock("../agents/agent-tools.js", () => ({ resolveToolLoopDetectionConfig: hookMocks.resolveToolLoopDetectionConfig, })); -vi.mock("../agents/pi-tools.before-tool-call.js", () => ({ +vi.mock("../agents/agent-tools.before-tool-call.js", () => ({ runBeforeToolCallHook: hookMocks.runBeforeToolCallHook, })); diff --git a/src/gateway/tools-invoke-shared.ts b/src/gateway/tools-invoke-shared.ts index a85b7096130..aa782c8dfa9 100644 --- a/src/gateway/tools-invoke-shared.ts +++ b/src/gateway/tools-invoke-shared.ts @@ -1,6 +1,6 @@ +import { runBeforeToolCallHook } from "../agents/agent-tools.before-tool-call.js"; +import { resolveToolLoopDetectionConfig } from "../agents/agent-tools.js"; import { getChannelAgentToolMeta } from "../agents/channel-tools.js"; -import { runBeforeToolCallHook } from "../agents/pi-tools.before-tool-call.js"; -import { resolveToolLoopDetectionConfig } from "../agents/pi-tools.js"; import { isKnownCoreToolId } from "../agents/tool-catalog.js"; import { ToolInputError, type AnyAgentTool } from "../agents/tools/common.js"; import { resolveMainSessionKey } from "../config/sessions.js"; diff --git a/src/hooks/bundled/session-memory/handler.test.ts b/src/hooks/bundled/session-memory/handler.test.ts index 3bcd2865b29..9da99f84130 100644 --- a/src/hooks/bundled/session-memory/handler.test.ts +++ b/src/hooks/bundled/session-memory/handler.test.ts @@ -13,7 +13,7 @@ import { getRecentSessionContentWithResetFallback, } from "./transcript.js"; -// Avoid calling the embedded Pi agent (global command lane); keep this unit test deterministic. +// Avoid calling the embedded OpenClaw agent (global command lane); keep this unit test deterministic. vi.mock("../../llm-slug-generator.js", () => ({ generateSlugViaLLM: vi.fn().mockResolvedValue("simple-math"), })); diff --git a/src/hooks/llm-slug-generator.test.ts b/src/hooks/llm-slug-generator.test.ts index 3de01408941..ef8d318e12b 100644 --- a/src/hooks/llm-slug-generator.test.ts +++ b/src/hooks/llm-slug-generator.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -const runEmbeddedPiAgentMock = vi.fn(); +const runEmbeddedAgentMock = vi.fn(); vi.mock("../agents/agent-scope.js", () => ({ resolveDefaultAgentId: vi.fn(() => "main"), @@ -16,28 +16,28 @@ vi.mock("../agents/agent-scope.js", () => ({ }), })); -vi.mock("../agents/pi-embedded.js", () => ({ - runEmbeddedPiAgent: (...args: unknown[]) => runEmbeddedPiAgentMock(...args), +vi.mock("../agents/embedded-agent.js", () => ({ + runEmbeddedAgent: (...args: unknown[]) => runEmbeddedAgentMock(...args), })); import { generateSlugViaLLM } from "./llm-slug-generator.js"; function requireFirstRunOptions(): Record { - const [call] = runEmbeddedPiAgentMock.mock.calls; + const [call] = runEmbeddedAgentMock.mock.calls; if (!call) { - throw new Error("expected embedded Pi agent run"); + throw new Error("expected embedded OpenClaw agent run"); } const [options] = call; if (!options || typeof options !== "object") { - throw new Error("expected embedded Pi agent run options"); + throw new Error("expected embedded OpenClaw agent run options"); } return options as Record; } describe("generateSlugViaLLM", () => { beforeEach(() => { - runEmbeddedPiAgentMock.mockReset(); - runEmbeddedPiAgentMock.mockResolvedValue({ + runEmbeddedAgentMock.mockReset(); + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "test-slug" }], }); }); @@ -48,7 +48,7 @@ describe("generateSlugViaLLM", () => { cfg: {} as OpenClawConfig, }); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); const options = requireFirstRunOptions(); expect(options.timeoutMs).toBe(15_000); expect(options.cleanupBundleMcpOnRunEnd).toBe(true); @@ -66,7 +66,7 @@ describe("generateSlugViaLLM", () => { } as OpenClawConfig, }); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); expect(requireFirstRunOptions().timeoutMs).toBe(500_000); }); @@ -100,7 +100,7 @@ describe("generateSlugViaLLM", () => { } as OpenClawConfig, }); - expect(runEmbeddedPiAgentMock).toHaveBeenCalledOnce(); + expect(runEmbeddedAgentMock).toHaveBeenCalledOnce(); const options = requireFirstRunOptions(); expect(options.provider).toBe("openai-codex"); expect(options.model).toBe("gpt-5.5"); diff --git a/src/hooks/llm-slug-generator.ts b/src/hooks/llm-slug-generator.ts index 7f656831c3f..245dac4e90d 100644 --- a/src/hooks/llm-slug-generator.ts +++ b/src/hooks/llm-slug-generator.ts @@ -10,8 +10,8 @@ import { resolveAgentWorkspaceDir, resolveAgentDir, } from "../agents/agent-scope.js"; +import { runEmbeddedAgent } from "../agents/embedded-agent.js"; import { resolveDefaultModelForAgent } from "../agents/model-selection.js"; -import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; import { resolveAgentTimeoutMs } from "../agents/timeout.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; @@ -59,7 +59,7 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design", }); const timeoutMs = resolveSlugGeneratorTimeoutMs(params.cfg); - const result = await runEmbeddedPiAgent({ + const result = await runEmbeddedAgent({ sessionId: `slug-generator-${Date.now()}`, sessionKey: "temp:slug-generator", agentId, diff --git a/src/infra/abort-pattern.test.ts b/src/infra/abort-pattern.test.ts index b233643699a..6ceeb9b3eb6 100644 --- a/src/infra/abort-pattern.test.ts +++ b/src/infra/abort-pattern.test.ts @@ -84,7 +84,7 @@ describe("abort pattern: .bind() vs arrow closure (#7174)", () => { }); it("bindAbortRelay() forwards abort through combined signals", () => { - // Simulates the combineAbortSignals pattern from pi-tools.abort.ts + // Simulates the combineAbortSignals pattern from agent-tools.abort.ts const signalA = new AbortController(); const signalB = new AbortController(); const combined = new AbortController(); diff --git a/src/infra/dotenv.test.ts b/src/infra/dotenv.test.ts index 935c4c49519..5bfe4d4db97 100644 --- a/src/infra/dotenv.test.ts +++ b/src/infra/dotenv.test.ts @@ -370,7 +370,7 @@ describe("loadDotEnv", () => { }); }); - it("blocks path-override vars (OPENCLAW_AGENT_DIR, OPENCLAW_BUNDLED_PLUGINS_DIR, PI_CODING_AGENT_DIR, OPENCLAW_OAUTH_DIR) from workspace .env", async () => { + it("blocks path-override vars (OPENCLAW_AGENT_DIR, OPENCLAW_BUNDLED_PLUGINS_DIR, OPENCLAW_OAUTH_DIR) from workspace .env", async () => { await withIsolatedEnvAndCwd(async () => { await withDotEnvFixture(async ({ base, cwdDir }) => { const bundledPluginsDir = path.join(base, "attacker-bundled"); @@ -379,21 +379,18 @@ describe("loadDotEnv", () => { [ "OPENCLAW_AGENT_DIR=./evil-agent", `OPENCLAW_BUNDLED_PLUGINS_DIR=${bundledPluginsDir}`, - "PI_CODING_AGENT_DIR=./evil-coding", "OPENCLAW_OAUTH_DIR=./evil-oauth", ].join("\n"), ); delete process.env.OPENCLAW_AGENT_DIR; delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; - delete process.env.PI_CODING_AGENT_DIR; delete process.env.OPENCLAW_OAUTH_DIR; loadWorkspaceDotEnvFile(path.join(cwdDir, ".env"), { quiet: true }); expect(process.env.OPENCLAW_AGENT_DIR).toBeUndefined(); expect(process.env.OPENCLAW_BUNDLED_PLUGINS_DIR).toBeUndefined(); - expect(process.env.PI_CODING_AGENT_DIR).toBeUndefined(); expect(process.env.OPENCLAW_OAUTH_DIR).toBeUndefined(); }); }); diff --git a/src/infra/dotenv.ts b/src/infra/dotenv.ts index 9f4f302b542..c58836af2c8 100644 --- a/src/infra/dotenv.ts +++ b/src/infra/dotenv.ts @@ -78,7 +78,6 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([ "OPENCLAW_SKIP_BROWSER_CONTROL_SERVER", "OPENCLAW_STATE_DIR", "OPENCLAW_TEST_TAILSCALE_BINARY", - "PI_CODING_AGENT_DIR", "PATH", "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH", "PROGRAMFILES", diff --git a/src/infra/heartbeat-runner.tool-response.test.ts b/src/infra/heartbeat-runner.tool-response.test.ts index 52e941b77ee..64253f8569a 100644 --- a/src/infra/heartbeat-runner.tool-response.test.ts +++ b/src/infra/heartbeat-runner.tool-response.test.ts @@ -260,6 +260,21 @@ describe("runHeartbeatOnce heartbeat response tool", () => { expectHeartbeatToolPrompt(result); }); + it.each([ + ["agentHarnessId", { agentHarnessId: "codex" }], + ["agentRuntimeOverride", { agentRuntimeOverride: "codex" }], + ])( + "preserves persisted Codex runtime from %s for non-OpenAI heartbeat sessions", + async (_field, session) => { + const result = await runPromptScenario({ + config: { model: "anthropic/claude-sonnet-4-6" }, + session, + }); + + expectHeartbeatToolPrompt(result); + }, + ); + it("delivers Codex runtime failure notices during Codex heartbeat message-tool mode", async () => { await withTempTelegramHeartbeatSandbox(async ({ tmpDir, storePath, replySpy }) => { const cfg = createConfig({ tmpDir, storePath }); @@ -356,7 +371,7 @@ describe("runHeartbeatOnce heartbeat response tool", () => { config: { agentRuntimeId: "codex", model: "openai/gpt-5.5", - modelRuntimeId: "pi", + modelRuntimeId: "native", }, }); diff --git a/src/infra/heartbeat-runner.ts b/src/infra/heartbeat-runner.ts index 3091e7d51dd..be382ecbf07 100644 --- a/src/infra/heartbeat-runner.ts +++ b/src/infra/heartbeat-runner.ts @@ -5,18 +5,19 @@ import { hasOutboundReplyContent, resolveSendableOutboundReplyParts, } from "openclaw/plugin-sdk/reply-payload"; +import { normalizeOptionalAgentRuntimeId } from "../agents/agent-runtime-id.js"; import { - listAgentEntries, listAgentIds, resolveAgentConfig, resolveAgentWorkspaceDir, resolveDefaultAgentId, } from "../agents/agent-scope.js"; import { appendCronStyleCurrentTimeLine } from "../agents/current-time.js"; +import { resolveEmbeddedSessionLane } from "../agents/embedded-agent-runner/lanes.js"; +import { formatReasoningMessage } from "../agents/embedded-agent-utils.js"; import { resolveAgentHarnessPolicy } from "../agents/harness/policy.js"; import { resolveModelRefFromString, type ModelRef } from "../agents/model-selection.js"; -import { resolveEmbeddedSessionLane } from "../agents/pi-embedded-runner/lanes.js"; -import { formatReasoningMessage } from "../agents/pi-embedded-utils.js"; +import { resolvePersistedSessionRuntimeId } from "../agents/session-runtime-compat.js"; import { DEFAULT_HEARTBEAT_FILENAME } from "../agents/workspace.js"; import { resolveHeartbeatReplyPayload } from "../auto-reply/heartbeat-reply-payload.js"; import { @@ -441,51 +442,19 @@ function resolveHeartbeatModelRef(params: { }; } -function normalizeHeartbeatRuntimeId(raw: string | undefined): string { - const normalized = normalizeLowercaseStringOrEmpty(raw); - return normalized === "codex-app-server" ? "codex" : normalized; -} - -function resolvePinnedHeartbeatRuntimeId(entry: SessionEntry | undefined): string { - const runtimeId = - normalizeHeartbeatRuntimeId(entry?.agentHarnessId) || - normalizeHeartbeatRuntimeId(entry?.agentRuntimeOverride); - return runtimeId === "auto" ? "" : runtimeId; -} - function usesCodexHarness(params: { cfg: OpenClawConfig; agentId: string; heartbeat?: HeartbeatConfig; entry?: SessionEntry; }): boolean { - const normalizedAgentId = normalizeAgentId(params.agentId); - const agentEntry = listAgentEntries(params.cfg).find( - (candidate) => normalizeAgentId(candidate.id) === normalizedAgentId, - ); - const runtimeId = - resolvePinnedHeartbeatRuntimeId(params.entry) || - normalizeHeartbeatRuntimeId(process.env.OPENCLAW_AGENT_RUNTIME) || - normalizeHeartbeatRuntimeId(agentEntry?.agentRuntime?.id) || - normalizeHeartbeatRuntimeId(agentEntry?.embeddedHarness?.runtime) || - resolveConfiguredHeartbeatModelRuntimeId(params) || - normalizeHeartbeatRuntimeId(params.cfg.agents?.defaults?.agentRuntime?.id) || - normalizeHeartbeatRuntimeId(params.cfg.agents?.defaults?.embeddedHarness?.runtime); - if (runtimeId === "codex") { + const persistedRuntimeId = resolvePersistedSessionRuntimeId(params.entry); + if (persistedRuntimeId === "codex") { return true; } - if (runtimeId && runtimeId !== "auto") { + if (persistedRuntimeId && persistedRuntimeId !== "auto") { return false; } - return normalizeLowercaseStringOrEmpty(resolveHeartbeatModelRef(params).provider) === "codex"; -} - -function resolveConfiguredHeartbeatModelRuntimeId(params: { - cfg: OpenClawConfig; - agentId: string; - heartbeat?: HeartbeatConfig; - entry?: SessionEntry; -}): string { const modelRef = resolveHeartbeatModelRef(params); const policy = resolveAgentHarnessPolicy({ config: params.cfg, @@ -493,11 +462,14 @@ function resolveConfiguredHeartbeatModelRuntimeId(params: { modelId: modelRef.model, agentId: params.agentId, }); - if (policy.runtimeSource === "implicit") { - return ""; + const runtimeId = normalizeOptionalAgentRuntimeId(policy.runtime); + if (runtimeId === "codex") { + return true; } - const runtime = normalizeHeartbeatRuntimeId(policy.runtime); - return runtime === "auto" ? "" : runtime; + if (runtimeId && runtimeId !== "auto") { + return false; + } + return normalizeLowercaseStringOrEmpty(modelRef.provider) === "codex"; } function shouldUseHeartbeatResponseToolPrompt(params: { diff --git a/src/infra/outbound/message-action-runner.ts b/src/infra/outbound/message-action-runner.ts index 24e9bba2953..2c05e604de3 100644 --- a/src/infra/outbound/message-action-runner.ts +++ b/src/infra/outbound/message-action-runner.ts @@ -1,6 +1,6 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload"; import { resolveSessionAgentId } from "../../agents/agent-scope.js"; +import type { AgentToolResult } from "../../agents/runtime/index.js"; import { readNumberParam, readStringArrayParam, diff --git a/src/infra/outbound/outbound-send-service.ts b/src/infra/outbound/outbound-send-service.ts index 50c7e70d410..4264e8d0f15 100644 --- a/src/infra/outbound/outbound-send-service.ts +++ b/src/infra/outbound/outbound-send-service.ts @@ -1,4 +1,4 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; +import type { AgentToolResult } from "../../agents/runtime/index.js"; import type { ReplyPayload } from "../../auto-reply/reply-payload.js"; import type { InboundEventKind } from "../../channels/inbound-event/kind.js"; import { dispatchChannelMessageAction } from "../../channels/plugins/message-action-dispatch.js"; diff --git a/src/infra/provider-usage.auth.normalizes-keys.test.ts b/src/infra/provider-usage.auth.normalizes-keys.test.ts index 854fdf0f967..b7da0659878 100644 --- a/src/infra/provider-usage.auth.normalizes-keys.test.ts +++ b/src/infra/provider-usage.auth.normalizes-keys.test.ts @@ -1,6 +1,5 @@ import nodeFs from "node:fs"; import fs from "node:fs/promises"; -import os from "node:os"; import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { NON_ENV_SECRETREF_MARKER } from "../agents/model-auth-markers.js"; @@ -157,33 +156,12 @@ const providerRuntimeMocks = vi.hoisted(() => ({ providerIds?: string[]; envDirect?: Array; }) => params.context.resolveApiKeyFromConfigAndStore(options); - const resolveLegacyZaiToken = (): string | null => { - const home = params.context.env?.HOME ?? params.context.env?.USERPROFILE; - if (!home) { - return null; - } - try { - const parsed = JSON.parse( - nodeFs.readFileSync(path.join(home, ".pi", "agent", "auth.json"), "utf8"), - ) as { - "z-ai"?: { access?: string }; - }; - return parsed["z-ai"]?.access ?? null; - } catch { - return null; - } - }; - if (params.provider === "zai") { const token = resolveToken({ providerIds: ["zai", "z-ai"], envDirect: [params.context.env?.ZAI_API_KEY, params.context.env?.Z_AI_API_KEY], }); - return token - ? { token } - : resolveLegacyZaiToken() - ? { token: resolveLegacyZaiToken()! } - : null; + return token ? { token } : null; } if (params.provider === "minimax") { @@ -375,12 +353,6 @@ describe("resolveProviderAuths key normalization", () => { ); } - async function writeLegacyPiAuth(home: string, raw: string) { - const legacyDir = path.join(home, ".pi", "agent"); - await fs.mkdir(legacyDir, { recursive: true }); - await fs.writeFile(path.join(legacyDir, "auth.json"), raw, "utf8"); - } - function createTestModelDefinition(): ModelDefinitionConfig { return { id: "test-model", @@ -521,21 +493,6 @@ describe("resolveProviderAuths key normalization", () => { expect(auths).toEqual([{ provider: "anthropic", token: "token-1", accountId: "acc-1" }]); }); - it("falls back to legacy .pi auth file for zai keys even after os.homedir() is primed", async () => { - // Prime os.homedir() to simulate long-lived workers that may have touched it before HOME changes. - os.homedir(); - await expectResolvedAuthsFromSuiteHome({ - providers: ["zai"], - setup: async (home) => { - await writeLegacyPiAuth( - home, - `${JSON.stringify({ "z-ai": { access: "legacy-zai-key" } }, null, 2)}\n`, - ); - }, - expected: [{ provider: "zai", token: "legacy-zai-key" }], - }); - }); - it.each([ { name: "extracts google oauth token from JSON payload in token profiles", @@ -621,16 +578,6 @@ describe("resolveProviderAuths key normalization", () => { }); }); - it("ignores invalid legacy z-ai auth files", async () => { - await expectResolvedAuthsFromSuiteHome({ - providers: ["zai"], - setup: async (home) => { - await writeLegacyPiAuth(home, "{not-json"); - }, - expected: [], - }); - }); - it("discovers oauth provider from config but skips mismatched profile providers", async () => { await withSuiteHome(async (home) => { const config = { diff --git a/src/infra/provider-usage.auth.plugin.test.ts b/src/infra/provider-usage.auth.plugin.test.ts index cae8459d434..fccbadfce0e 100644 --- a/src/infra/provider-usage.auth.plugin.test.ts +++ b/src/infra/provider-usage.auth.plugin.test.ts @@ -147,63 +147,6 @@ describe("resolveProviderAuths plugin boundary", () => { expect(ensureAuthProfileStoreMock).not.toHaveBeenCalled(); }); - it("keeps plugin usage auth when a shared legacy plugin credential source exists", async () => { - await withTempHome(async (homeDir) => { - fs.mkdirSync(path.join(homeDir, ".pi", "agent"), { recursive: true }); - fs.writeFileSync( - path.join(homeDir, ".pi", "agent", "auth.json"), - `${JSON.stringify({ "z-ai": { access: "legacy-zai-token" } })}\n`, - ); - resolveProviderUsageAuthWithPluginMock.mockResolvedValueOnce({ - token: "legacy-zai-token", - }); - await expect( - resolveProviderAuthsForTest({ - providers: ["zai"], - skipPluginAuthWithoutCredentialSource: true, - env: { HOME: homeDir }, - }), - ).resolves.toEqual([ - { - provider: "zai", - token: "legacy-zai-token", - }, - ]); - }); - - expect(providerCalls(resolveProviderUsageAuthWithPluginMock)).toEqual(["zai"]); - expect(ensureAuthProfileStoreMock).not.toHaveBeenCalled(); - }); - - it("keeps legacy plugin credential sources provider-specific", async () => { - await withTempHome(async (homeDir) => { - fs.mkdirSync(path.join(homeDir, ".pi", "agent"), { recursive: true }); - fs.writeFileSync( - path.join(homeDir, ".pi", "agent", "auth.json"), - `${JSON.stringify({ "z-ai": { access: "legacy-zai-token" } })}\n`, - ); - resolveProviderUsageAuthWithPluginMock.mockResolvedValueOnce({ - token: "legacy-zai-token", - }); - - await expect( - resolveProviderAuthsForTest({ - providers: ["anthropic", "zai"], - skipPluginAuthWithoutCredentialSource: true, - env: { HOME: homeDir }, - }), - ).resolves.toEqual([ - { - provider: "zai", - token: "legacy-zai-token", - }, - ]); - }); - - expect(resolveProviderUsageAuthWithPluginMock).toHaveBeenCalledTimes(1); - expect(providerCalls(resolveProviderUsageAuthWithPluginMock)).toEqual(["zai"]); - }); - it("keeps auth-profile credential sources provider-specific", async () => { hasAnyAuthProfileStoreSourceMock.mockReturnValue(true); ensureAuthProfileStoreWithoutExternalProfilesMock.mockReturnValue({ diff --git a/src/infra/provider-usage.shared.test.ts b/src/infra/provider-usage.shared.test.ts index 3538d0f77bc..a5b5fe37e61 100644 --- a/src/infra/provider-usage.shared.test.ts +++ b/src/infra/provider-usage.shared.test.ts @@ -1,24 +1,5 @@ -import fs from "node:fs/promises"; -import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { withTempDir } from "../test-helpers/temp-dir.js"; -import { - clampPercent, - resolveLegacyPiAgentAccessToken, - resolveUsageProviderId, - withTimeout, -} from "./provider-usage.shared.js"; - -async function withLegacyPiAuthFile( - contents: string, - run: (home: string) => Promise | void, -): Promise { - await withTempDir({ prefix: "openclaw-provider-usage-" }, async (home) => { - await fs.mkdir(path.join(home, ".pi", "agent"), { recursive: true }); - await fs.writeFile(path.join(home, ".pi", "agent", "auth.json"), contents, "utf8"); - await run(home); - }); -} +import { clampPercent, resolveUsageProviderId, withTimeout } from "./provider-usage.shared.js"; describe("provider-usage.shared", () => { afterEach(() => { @@ -83,21 +64,4 @@ describe("provider-usage.shared", () => { expect(clearTimeoutSpy).toHaveBeenCalledTimes(1); }); - - it.each([ - { - name: "reads legacy pi auth tokens for known provider aliases", - contents: `${JSON.stringify({ "z-ai": { access: "legacy-zai-key" } }, null, 2)}\n`, - expected: "legacy-zai-key", - }, - { - name: "returns undefined for invalid legacy pi auth files", - contents: "{not-json", - expected: undefined, - }, - ])("$name", async ({ contents, expected }) => { - await withLegacyPiAuthFile(contents, async (home) => { - expect(resolveLegacyPiAgentAccessToken({ HOME: home }, ["z-ai", "zai"])).toBe(expected); - }); - }); }); diff --git a/src/infra/restart-coordinator.ts b/src/infra/restart-coordinator.ts index f7dd64aecfe..dc4881bbf81 100644 --- a/src/infra/restart-coordinator.ts +++ b/src/infra/restart-coordinator.ts @@ -1,4 +1,4 @@ -import { getActiveEmbeddedRunCount } from "../agents/pi-embedded-runner/run-state.js"; +import { getActiveEmbeddedRunCount } from "../agents/embedded-agent-runner/run-state.js"; import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js"; import { getTotalQueueSize } from "../process/command-queue.js"; import { diff --git a/src/infra/state-migrations.ts b/src/infra/state-migrations.ts index bc89853abed..42eda61fa40 100644 --- a/src/infra/state-migrations.ts +++ b/src/infra/state-migrations.ts @@ -1502,7 +1502,7 @@ export async function autoMigrateLegacyState(params: { } }; - if (env.OPENCLAW_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim()) { + if (env.OPENCLAW_AGENT_DIR?.trim()) { const changes = [...stateDirResult.changes, ...orphanKeys.changes]; const warnings = [...stateDirResult.warnings, ...orphanKeys.warnings]; logMigrationResults(changes, warnings); diff --git a/src/infra/tsdown-config.test.ts b/src/infra/tsdown-config.test.ts index e9de937e4d1..34a67f9bc18 100644 --- a/src/infra/tsdown-config.test.ts +++ b/src/infra/tsdown-config.test.ts @@ -102,7 +102,6 @@ describe("tsdown config", () => { "plugins/memory-state", "subagent-registry.runtime", "task-registry-control.runtime", - "agents/pi-model-discovery-runtime", "link-understanding/apply.runtime", "media-understanding/apply.runtime", "index", diff --git a/src/logging/diagnostic-stuck-session-recovery.integration.test.ts b/src/logging/diagnostic-stuck-session-recovery.integration.test.ts index e0d939fad13..1379c4fed26 100644 --- a/src/logging/diagnostic-stuck-session-recovery.integration.test.ts +++ b/src/logging/diagnostic-stuck-session-recovery.integration.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it } from "vitest"; -import { resolveEmbeddedSessionLane } from "../agents/pi-embedded-runner/lanes.js"; +import { resolveEmbeddedSessionLane } from "../agents/embedded-agent-runner/lanes.js"; import { testing as embeddedRunTesting, clearActiveEmbeddedRun, diff --git a/src/logging/diagnostic-stuck-session-recovery.runtime.test.ts b/src/logging/diagnostic-stuck-session-recovery.runtime.test.ts index bf8f89290af..b4781229acd 100644 --- a/src/logging/diagnostic-stuck-session-recovery.runtime.test.ts +++ b/src/logging/diagnostic-stuck-session-recovery.runtime.test.ts @@ -4,10 +4,10 @@ import path from "node:path"; import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ - abortEmbeddedPiRun: vi.fn(), - forceClearEmbeddedPiRun: vi.fn(), - isEmbeddedPiRunActive: vi.fn(), - isEmbeddedPiRunHandleActive: vi.fn(), + abortEmbeddedAgentRun: vi.fn(), + forceClearEmbeddedAgentRun: vi.fn(), + isEmbeddedAgentRunActive: vi.fn(), + isEmbeddedAgentRunHandleActive: vi.fn(), getCommandLaneSnapshot: vi.fn(), resetCommandLane: vi.fn(), resolveActiveEmbeddedRunSessionId: vi.fn(), @@ -15,7 +15,7 @@ const mocks = vi.hoisted(() => ({ resolveActiveEmbeddedRunHandleSessionId: vi.fn(), resolveActiveEmbeddedRunHandleSessionIdBySessionFile: vi.fn(), resolveEmbeddedSessionLane: vi.fn((key: string) => `session:${key}`), - waitForEmbeddedPiRunEnd: vi.fn(), + waitForEmbeddedAgentRunEnd: vi.fn(), getDiagnosticSessionActivitySnapshot: vi.fn(), diag: { debug: vi.fn(), @@ -23,38 +23,38 @@ const mocks = vi.hoisted(() => ({ }, })); -vi.mock("../agents/pi-embedded-runner/runs.js", () => ({ - abortAndDrainEmbeddedPiRun: async (params: { +vi.mock("../agents/embedded-agent-runner/runs.js", () => ({ + abortAndDrainEmbeddedAgentRun: async (params: { sessionId: string; sessionKey?: string; settleMs?: number; forceClear?: boolean; reason?: string; }) => { - const aborted = mocks.abortEmbeddedPiRun(params.sessionId); + const aborted = mocks.abortEmbeddedAgentRun(params.sessionId); const drained = aborted - ? await mocks.waitForEmbeddedPiRunEnd(params.sessionId, params.settleMs) + ? await mocks.waitForEmbeddedAgentRunEnd(params.sessionId, params.settleMs) : false; const forceCleared = params.forceClear === true && (!aborted || !drained) - ? mocks.forceClearEmbeddedPiRun(params.sessionId, params.sessionKey, params.reason) + ? mocks.forceClearEmbeddedAgentRun(params.sessionId, params.sessionKey, params.reason) : false; return { aborted, drained, forceCleared }; }, - abortEmbeddedPiRun: mocks.abortEmbeddedPiRun, - forceClearEmbeddedPiRun: mocks.forceClearEmbeddedPiRun, - isEmbeddedPiRunActive: mocks.isEmbeddedPiRunActive, - isEmbeddedPiRunHandleActive: mocks.isEmbeddedPiRunHandleActive, + abortEmbeddedAgentRun: mocks.abortEmbeddedAgentRun, + forceClearEmbeddedAgentRun: mocks.forceClearEmbeddedAgentRun, + isEmbeddedAgentRunActive: mocks.isEmbeddedAgentRunActive, + isEmbeddedAgentRunHandleActive: mocks.isEmbeddedAgentRunHandleActive, resolveActiveEmbeddedRunSessionId: mocks.resolveActiveEmbeddedRunSessionId, resolveActiveEmbeddedRunSessionIdBySessionFile: mocks.resolveActiveEmbeddedRunSessionIdBySessionFile, resolveActiveEmbeddedRunHandleSessionId: mocks.resolveActiveEmbeddedRunHandleSessionId, resolveActiveEmbeddedRunHandleSessionIdBySessionFile: mocks.resolveActiveEmbeddedRunHandleSessionIdBySessionFile, - waitForEmbeddedPiRunEnd: mocks.waitForEmbeddedPiRunEnd, + waitForEmbeddedAgentRunEnd: mocks.waitForEmbeddedAgentRunEnd, })); -vi.mock("../agents/pi-embedded-runner/lanes.js", () => ({ +vi.mock("../agents/embedded-agent-runner/lanes.js", () => ({ resolveEmbeddedSessionLane: mocks.resolveEmbeddedSessionLane, })); @@ -78,10 +78,10 @@ import { function resetMocks() { testing.resetRecoveriesInFlight(); - mocks.abortEmbeddedPiRun.mockReset(); - mocks.forceClearEmbeddedPiRun.mockReset(); - mocks.isEmbeddedPiRunActive.mockReset(); - mocks.isEmbeddedPiRunHandleActive.mockReset(); + mocks.abortEmbeddedAgentRun.mockReset(); + mocks.forceClearEmbeddedAgentRun.mockReset(); + mocks.isEmbeddedAgentRunActive.mockReset(); + mocks.isEmbeddedAgentRunHandleActive.mockReset(); mocks.getCommandLaneSnapshot.mockReset(); mocks.getCommandLaneSnapshot.mockReturnValue({ lane: "session:agent:main:main", @@ -97,10 +97,8 @@ function resetMocks() { mocks.resolveActiveEmbeddedRunHandleSessionId.mockReset(); mocks.resolveActiveEmbeddedRunHandleSessionIdBySessionFile.mockReset(); mocks.resolveEmbeddedSessionLane.mockClear(); - mocks.waitForEmbeddedPiRunEnd.mockReset(); + mocks.waitForEmbeddedAgentRunEnd.mockReset(); mocks.getDiagnosticSessionActivitySnapshot.mockReset(); - // Default: no progress signal, so the staleness gate stays off unless a test - // opts in by returning a stale lastProgressAgeMs. mocks.getDiagnosticSessionActivitySnapshot.mockReturnValue({}); mocks.diag.debug.mockReset(); mocks.diag.warn.mockReset(); @@ -128,9 +126,9 @@ describe("stuck session recovery", () => { queueDepth: 1, }); - expect(mocks.abortEmbeddedPiRun).not.toHaveBeenCalled(); - expect(mocks.waitForEmbeddedPiRunEnd).not.toHaveBeenCalled(); - expect(mocks.forceClearEmbeddedPiRun).not.toHaveBeenCalled(); + expect(mocks.abortEmbeddedAgentRun).not.toHaveBeenCalled(); + expect(mocks.waitForEmbeddedAgentRunEnd).not.toHaveBeenCalled(); + expect(mocks.forceClearEmbeddedAgentRun).not.toHaveBeenCalled(); expect(mocks.resetCommandLane).not.toHaveBeenCalled(); expect(warnLogMessages()).toEqual([ "stuck session recovery skipped: sessionId=session-1 sessionKey=agent:main:main age=180s queueDepth=1 activeSessionId=session-1", @@ -156,7 +154,7 @@ describe("stuck session recovery", () => { reason: "active_embedded_run", activeSessionId: "session-file-run", }); - expect(mocks.abortEmbeddedPiRun).not.toHaveBeenCalled(); + expect(mocks.abortEmbeddedAgentRun).not.toHaveBeenCalled(); expect(mocks.resetCommandLane).not.toHaveBeenCalled(); }); @@ -165,8 +163,8 @@ describe("stuck session recovery", () => { mocks.getDiagnosticSessionActivitySnapshot.mockReturnValue({ lastProgressAgeMs: 10 * 60_000, }); - mocks.abortEmbeddedPiRun.mockReturnValue(true); - mocks.waitForEmbeddedPiRunEnd.mockResolvedValue(true); + mocks.abortEmbeddedAgentRun.mockReturnValue(true); + mocks.waitForEmbeddedAgentRunEnd.mockResolvedValue(true); const outcome = await recoverStuckDiagnosticSession({ sessionId: "session-1", @@ -175,15 +173,14 @@ describe("stuck session recovery", () => { queueDepth: 1, }); - expect(mocks.abortEmbeddedPiRun).toHaveBeenCalledWith("session-1"); + expect(mocks.abortEmbeddedAgentRun).toHaveBeenCalledWith("session-1"); expect(outcome.status).toBe("aborted"); expect(warnLogMessages().some((m) => m.includes("reclaiming stale active run"))).toBe(true); }); - it("aborts an active embedded run when active abort recovery is enabled", async () => { mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue("session-1"); - mocks.abortEmbeddedPiRun.mockReturnValue(true); - mocks.waitForEmbeddedPiRunEnd.mockResolvedValue(true); + mocks.abortEmbeddedAgentRun.mockReturnValue(true); + mocks.waitForEmbeddedAgentRunEnd.mockResolvedValue(true); await recoverStuckDiagnosticSession({ sessionId: "session-1", @@ -192,16 +189,16 @@ describe("stuck session recovery", () => { allowActiveAbort: true, }); - expect(mocks.abortEmbeddedPiRun).toHaveBeenCalledWith("session-1"); - expect(mocks.waitForEmbeddedPiRunEnd).toHaveBeenCalledWith("session-1", 15_000); - expect(mocks.forceClearEmbeddedPiRun).not.toHaveBeenCalled(); + expect(mocks.abortEmbeddedAgentRun).toHaveBeenCalledWith("session-1"); + expect(mocks.waitForEmbeddedAgentRunEnd).toHaveBeenCalledWith("session-1", 15_000); + expect(mocks.forceClearEmbeddedAgentRun).not.toHaveBeenCalled(); expect(mocks.resetCommandLane).not.toHaveBeenCalled(); }); it("returns an abort outcome for a stale tool call on an active embedded run", async () => { mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue("session-tool"); - mocks.abortEmbeddedPiRun.mockReturnValue(true); - mocks.waitForEmbeddedPiRunEnd.mockResolvedValue(true); + mocks.abortEmbeddedAgentRun.mockReturnValue(true); + mocks.waitForEmbeddedAgentRunEnd.mockResolvedValue(true); const outcome = await recoverStuckDiagnosticSession({ sessionId: "session-tool", @@ -223,8 +220,8 @@ describe("stuck session recovery", () => { forceCleared: false, released: 0, }); - expect(mocks.abortEmbeddedPiRun).toHaveBeenCalledWith("session-tool"); - expect(mocks.waitForEmbeddedPiRunEnd).toHaveBeenCalledWith("session-tool", 15_000); + expect(mocks.abortEmbeddedAgentRun).toHaveBeenCalledWith("session-tool"); + expect(mocks.waitForEmbeddedAgentRunEnd).toHaveBeenCalledWith("session-tool", 15_000); expect(mocks.resetCommandLane).not.toHaveBeenCalled(); }); @@ -250,8 +247,8 @@ describe("stuck session recovery", () => { }) + "\n", ); mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue("run-456"); - mocks.abortEmbeddedPiRun.mockReturnValue(true); - mocks.waitForEmbeddedPiRunEnd.mockResolvedValue(true); + mocks.abortEmbeddedAgentRun.mockReturnValue(true); + mocks.waitForEmbeddedAgentRunEnd.mockResolvedValue(true); await recoverStuckDiagnosticSession({ sessionId: "run-456", @@ -276,8 +273,8 @@ describe("stuck session recovery", () => { it("force-clears and releases the session lane when abort cleanup does not drain", async () => { mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue("session-1"); - mocks.abortEmbeddedPiRun.mockReturnValue(true); - mocks.waitForEmbeddedPiRunEnd.mockResolvedValue(false); + mocks.abortEmbeddedAgentRun.mockReturnValue(true); + mocks.waitForEmbeddedAgentRunEnd.mockResolvedValue(false); mocks.resetCommandLane.mockReturnValue(1); await recoverStuckDiagnosticSession({ @@ -287,7 +284,7 @@ describe("stuck session recovery", () => { allowActiveAbort: true, }); - expect(mocks.forceClearEmbeddedPiRun).toHaveBeenCalledWith( + expect(mocks.forceClearEmbeddedAgentRun).toHaveBeenCalledWith( "session-1", "agent:main:main", "stuck_recovery", @@ -297,7 +294,7 @@ describe("stuck session recovery", () => { it("force-clears and releases the session lane when an active run cannot be aborted", async () => { mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue("session-1"); - mocks.abortEmbeddedPiRun.mockReturnValue(false); + mocks.abortEmbeddedAgentRun.mockReturnValue(false); mocks.resetCommandLane.mockReturnValue(1); await recoverStuckDiagnosticSession({ @@ -307,8 +304,8 @@ describe("stuck session recovery", () => { allowActiveAbort: true, }); - expect(mocks.waitForEmbeddedPiRunEnd).not.toHaveBeenCalled(); - expect(mocks.forceClearEmbeddedPiRun).toHaveBeenCalledWith( + expect(mocks.waitForEmbeddedAgentRunEnd).not.toHaveBeenCalled(); + expect(mocks.forceClearEmbeddedAgentRun).toHaveBeenCalledWith( "session-1", "agent:main:main", "stuck_recovery", @@ -326,15 +323,15 @@ describe("stuck session recovery", () => { ageMs: 180_000, }); - expect(mocks.abortEmbeddedPiRun).not.toHaveBeenCalled(); + expect(mocks.abortEmbeddedAgentRun).not.toHaveBeenCalled(); expect(mocks.resetCommandLane).toHaveBeenCalledWith("session:agent:main:main"); }); it("does not release the session lane while reply work is active without an embedded handle", async () => { mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue("queued-reply-session"); mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(true); - mocks.isEmbeddedPiRunHandleActive.mockReturnValue(false); + mocks.isEmbeddedAgentRunActive.mockReturnValue(true); + mocks.isEmbeddedAgentRunHandleActive.mockReturnValue(false); await recoverStuckDiagnosticSession({ sessionId: "queued-reply-session", @@ -343,122 +340,21 @@ describe("stuck session recovery", () => { queueDepth: 1, }); - expect(mocks.abortEmbeddedPiRun).not.toHaveBeenCalled(); - expect(mocks.forceClearEmbeddedPiRun).not.toHaveBeenCalled(); + expect(mocks.abortEmbeddedAgentRun).not.toHaveBeenCalled(); + expect(mocks.forceClearEmbeddedAgentRun).not.toHaveBeenCalled(); expect(mocks.resetCommandLane).not.toHaveBeenCalled(); expect(warnLogMessages()).toEqual([ "stuck session recovery outcome: status=skipped action=keep_lane sessionId=queued-reply-session sessionKey=agent:main:main activeSessionId=queued-reply-session activeWorkKind=embedded_run reason=active_reply_work", ]); }); - it("reclaims stale leaked reply work with queued work and no forward progress (#85639)", async () => { - mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue("queued-reply-session"); - mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(true); - mocks.isEmbeddedPiRunHandleActive.mockReturnValue(false); - // The "active" run has made no forward progress for well past the staleness - // window — a leaked/dead handle, not genuine work. - mocks.getDiagnosticSessionActivitySnapshot.mockReturnValue({ lastProgressAgeMs: 10 * 60_000 }); - mocks.abortEmbeddedPiRun.mockReturnValue(true); - mocks.waitForEmbeddedPiRunEnd.mockResolvedValue(true); - - const outcome = await recoverStuckDiagnosticSession({ - sessionId: "queued-reply-session", - sessionKey: "agent:main:main", - ageMs: 180_000, - queueDepth: 1, - }); - - // Reclaimed (aborted) instead of skipping with active_reply_work. - expect(mocks.abortEmbeddedPiRun).toHaveBeenCalledWith("queued-reply-session"); - expect(outcome.status).not.toBe("skipped"); - expect(warnLogMessages().some((m) => m.includes("reclaiming stale active reply work"))).toBe( - true, - ); - }); - - it("honors an operator-raised stuck-session abort threshold for stale reclaim (#85639)", async () => { - mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue("queued-reply-session"); - mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(true); - mocks.isEmbeddedPiRunHandleActive.mockReturnValue(false); - mocks.abortEmbeddedPiRun.mockReturnValue(true); - mocks.waitForEmbeddedPiRunEnd.mockResolvedValue(true); - - // Operator raised the abort threshold to 20 min to protect slow active work. - const raisedAbortMs = 20 * 60_000; - - // Below the raised threshold (10 min): keep the lane, do not reclaim. - mocks.getDiagnosticSessionActivitySnapshot.mockReturnValue({ lastProgressAgeMs: 10 * 60_000 }); - const kept = await recoverStuckDiagnosticSession({ - sessionId: "queued-reply-session", - sessionKey: "agent:main:main", - ageMs: 180_000, - queueDepth: 1, - staleActiveProgressAbortMs: raisedAbortMs, - }); - expect(mocks.abortEmbeddedPiRun).not.toHaveBeenCalled(); - expect(kept.status).toBe("skipped"); - - // Past the raised threshold (25 min): reclaim. - mocks.getDiagnosticSessionActivitySnapshot.mockReturnValue({ lastProgressAgeMs: 25 * 60_000 }); - const reclaimed = await recoverStuckDiagnosticSession({ - sessionId: "queued-reply-session", - sessionKey: "agent:main:main", - ageMs: 180_000, - queueDepth: 1, - staleActiveProgressAbortMs: raisedAbortMs, - }); - expect(mocks.abortEmbeddedPiRun).toHaveBeenCalledWith("queued-reply-session"); - expect(reclaimed.status).not.toBe("skipped"); - }); - - it("keeps the lane when active reply work is still progressing", async () => { - mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue("queued-reply-session"); - mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(true); - mocks.isEmbeddedPiRunHandleActive.mockReturnValue(false); - // Recent forward progress: a genuinely active run must not be reclaimed. - mocks.getDiagnosticSessionActivitySnapshot.mockReturnValue({ lastProgressAgeMs: 5_000 }); - - const outcome = await recoverStuckDiagnosticSession({ - sessionId: "queued-reply-session", - sessionKey: "agent:main:main", - ageMs: 180_000, - queueDepth: 1, - }); - - expect(mocks.abortEmbeddedPiRun).not.toHaveBeenCalled(); - expect(outcome.status).toBe("skipped"); - expect(warnLogMessages().some((m) => m.includes("reason=active_reply_work"))).toBe(true); - }); - - it("does not reclaim stale reply work when no work is queued", async () => { - mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue("queued-reply-session"); - mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(true); - mocks.isEmbeddedPiRunHandleActive.mockReturnValue(false); - mocks.getDiagnosticSessionActivitySnapshot.mockReturnValue({ lastProgressAgeMs: 10 * 60_000 }); - - const outcome = await recoverStuckDiagnosticSession({ - sessionId: "queued-reply-session", - sessionKey: "agent:main:main", - ageMs: 180_000, - queueDepth: 0, - }); - - expect(mocks.abortEmbeddedPiRun).not.toHaveBeenCalled(); - expect(outcome.status).toBe("skipped"); - expect(warnLogMessages().some((m) => m.includes("reason=active_reply_work"))).toBe(true); - }); - it("aborts stale reply work without an embedded handle when active abort recovery is enabled", async () => { mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue("queued-reply-session"); mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(true); - mocks.isEmbeddedPiRunHandleActive.mockReturnValue(false); - mocks.abortEmbeddedPiRun.mockReturnValue(true); - mocks.waitForEmbeddedPiRunEnd.mockResolvedValue(true); + mocks.isEmbeddedAgentRunActive.mockReturnValue(true); + mocks.isEmbeddedAgentRunHandleActive.mockReturnValue(false); + mocks.abortEmbeddedAgentRun.mockReturnValue(true); + mocks.waitForEmbeddedAgentRunEnd.mockResolvedValue(true); await recoverStuckDiagnosticSession({ sessionId: "queued-reply-session", @@ -468,9 +364,9 @@ describe("stuck session recovery", () => { allowActiveAbort: true, }); - expect(mocks.abortEmbeddedPiRun).toHaveBeenCalledWith("queued-reply-session"); - expect(mocks.waitForEmbeddedPiRunEnd).toHaveBeenCalledWith("queued-reply-session", 15_000); - expect(mocks.forceClearEmbeddedPiRun).not.toHaveBeenCalled(); + expect(mocks.abortEmbeddedAgentRun).toHaveBeenCalledWith("queued-reply-session"); + expect(mocks.waitForEmbeddedAgentRunEnd).toHaveBeenCalledWith("queued-reply-session", 15_000); + expect(mocks.forceClearEmbeddedAgentRun).not.toHaveBeenCalled(); expect(mocks.resetCommandLane).not.toHaveBeenCalled(); expect(warnLogMessages()).toEqual([ "stuck session recovery: sessionId=queued-reply-session sessionKey=agent:main:main age=720s action=abort_embedded_run aborted=true drained=true released=0", @@ -481,11 +377,11 @@ describe("stuck session recovery", () => { it("reports queued lane work when aborting active work releases a lane", async () => { mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue("queued-reply-session"); mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(true); - mocks.isEmbeddedPiRunHandleActive.mockReturnValue(false); - mocks.abortEmbeddedPiRun.mockReturnValue(false); - mocks.forceClearEmbeddedPiRun.mockReturnValue(true); - mocks.waitForEmbeddedPiRunEnd.mockResolvedValue(false); + mocks.isEmbeddedAgentRunActive.mockReturnValue(true); + mocks.isEmbeddedAgentRunHandleActive.mockReturnValue(false); + mocks.abortEmbeddedAgentRun.mockReturnValue(false); + mocks.forceClearEmbeddedAgentRun.mockReturnValue(true); + mocks.waitForEmbeddedAgentRunEnd.mockResolvedValue(false); mocks.resetCommandLane.mockReturnValue(1); mocks.getCommandLaneSnapshot.mockReturnValue({ lane: "session:agent:main:main", @@ -518,8 +414,8 @@ describe("stuck session recovery", () => { it("does not release the session lane while unregistered lane work is active", async () => { mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue(undefined); mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(false); - mocks.isEmbeddedPiRunHandleActive.mockReturnValue(false); + mocks.isEmbeddedAgentRunActive.mockReturnValue(false); + mocks.isEmbeddedAgentRunHandleActive.mockReturnValue(false); mocks.getCommandLaneSnapshot.mockReturnValue({ lane: "session:agent:main:main", queuedCount: 1, @@ -536,8 +432,8 @@ describe("stuck session recovery", () => { queueDepth: 1, }); - expect(mocks.abortEmbeddedPiRun).not.toHaveBeenCalled(); - expect(mocks.forceClearEmbeddedPiRun).not.toHaveBeenCalled(); + expect(mocks.abortEmbeddedAgentRun).not.toHaveBeenCalled(); + expect(mocks.forceClearEmbeddedAgentRun).not.toHaveBeenCalled(); expect(mocks.resetCommandLane).not.toHaveBeenCalled(); expect(warnLogMessages()).toEqual([ "stuck session recovery outcome: status=skipped action=keep_lane sessionId=unregistered-work-session sessionKey=agent:main:main lane=session:agent:main:main reason=active_lane_task laneActive=1 laneQueued=1", @@ -547,7 +443,7 @@ describe("stuck session recovery", () => { it("reports when recovery finds no active work to release", async () => { mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(false); + mocks.isEmbeddedAgentRunActive.mockReturnValue(false); mocks.resetCommandLane.mockReturnValue(0); await recoverStuckDiagnosticSession({ @@ -565,7 +461,7 @@ describe("stuck session recovery", () => { it("clears stale queued processing state even when the lane has no active work", async () => { mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(false); + mocks.isEmbeddedAgentRunActive.mockReturnValue(false); mocks.resetCommandLane.mockReturnValue(0); await recoverStuckDiagnosticSession({ @@ -585,7 +481,7 @@ describe("stuck session recovery", () => { it("releases idle queued work without aborting when stale activity has no active owner", async () => { mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(false); + mocks.isEmbeddedAgentRunActive.mockReturnValue(false); mocks.resetCommandLane.mockReturnValue(0); const outcome = await recoverStuckDiagnosticSession({ @@ -603,15 +499,15 @@ describe("stuck session recovery", () => { sessionKey: "agent:main:main", released: 0, }); - expect(mocks.abortEmbeddedPiRun).not.toHaveBeenCalled(); - expect(mocks.forceClearEmbeddedPiRun).not.toHaveBeenCalled(); + expect(mocks.abortEmbeddedAgentRun).not.toHaveBeenCalled(); + expect(mocks.forceClearEmbeddedAgentRun).not.toHaveBeenCalled(); expect(mocks.resetCommandLane).toHaveBeenCalledWith("session:agent:main:main"); }); it("releases idle queued work with orphaned tool_call without aborting active work", async () => { mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue(undefined); mocks.resolveActiveEmbeddedRunSessionId.mockReturnValue(undefined); - mocks.isEmbeddedPiRunActive.mockReturnValue(false); + mocks.isEmbeddedAgentRunActive.mockReturnValue(false); mocks.resetCommandLane.mockReturnValue(1); const outcome = await recoverStuckDiagnosticSession({ @@ -629,13 +525,13 @@ describe("stuck session recovery", () => { sessionKey: "agent:sub:tool-runner", released: 1, }); - expect(mocks.abortEmbeddedPiRun).not.toHaveBeenCalled(); - expect(mocks.forceClearEmbeddedPiRun).not.toHaveBeenCalled(); + expect(mocks.abortEmbeddedAgentRun).not.toHaveBeenCalled(); + expect(mocks.forceClearEmbeddedAgentRun).not.toHaveBeenCalled(); expect(mocks.resetCommandLane).toHaveBeenCalledWith("session:agent:sub:tool-runner"); }); it("releases a stale session-id lane when no session key is available", async () => { - mocks.isEmbeddedPiRunHandleActive.mockReturnValue(false); + mocks.isEmbeddedAgentRunHandleActive.mockReturnValue(false); mocks.resetCommandLane.mockReturnValue(1); await recoverStuckDiagnosticSession({ @@ -643,7 +539,7 @@ describe("stuck session recovery", () => { ageMs: 180_000, }); - expect(mocks.abortEmbeddedPiRun).not.toHaveBeenCalled(); + expect(mocks.abortEmbeddedAgentRun).not.toHaveBeenCalled(); expect(mocks.resolveEmbeddedSessionLane).toHaveBeenCalledWith("session-only"); expect(mocks.resetCommandLane).toHaveBeenCalledWith("session:session-only"); }); @@ -654,8 +550,8 @@ describe("stuck session recovery", () => { resolveWait = resolve; }); mocks.resolveActiveEmbeddedRunHandleSessionId.mockReturnValue("session-1"); - mocks.abortEmbeddedPiRun.mockReturnValue(true); - mocks.waitForEmbeddedPiRunEnd.mockReturnValue(waitPromise); + mocks.abortEmbeddedAgentRun.mockReturnValue(true); + mocks.waitForEmbeddedAgentRunEnd.mockReturnValue(waitPromise); const first = recoverStuckDiagnosticSession({ sessionId: "session-1", @@ -670,7 +566,7 @@ describe("stuck session recovery", () => { allowActiveAbort: true, }); - expect(mocks.abortEmbeddedPiRun).toHaveBeenCalledTimes(1); + expect(mocks.abortEmbeddedAgentRun).toHaveBeenCalledTimes(1); if (!resolveWait) { throw new Error("Expected diagnostic recovery wait resolver to be initialized"); } diff --git a/src/logging/diagnostic-stuck-session-recovery.runtime.ts b/src/logging/diagnostic-stuck-session-recovery.runtime.ts index 6107af5caf9..7b88a7bd1ab 100644 --- a/src/logging/diagnostic-stuck-session-recovery.runtime.ts +++ b/src/logging/diagnostic-stuck-session-recovery.runtime.ts @@ -1,13 +1,13 @@ -import { resolveEmbeddedSessionLane } from "../agents/pi-embedded-runner/lanes.js"; +import { resolveEmbeddedSessionLane } from "../agents/embedded-agent-runner/lanes.js"; import { - abortAndDrainEmbeddedPiRun, - isEmbeddedPiRunActive, - isEmbeddedPiRunHandleActive, + abortAndDrainEmbeddedAgentRun, + isEmbeddedAgentRunActive, + isEmbeddedAgentRunHandleActive, resolveActiveEmbeddedRunSessionId, resolveActiveEmbeddedRunSessionIdBySessionFile, resolveActiveEmbeddedRunHandleSessionId, resolveActiveEmbeddedRunHandleSessionIdBySessionFile, -} from "../agents/pi-embedded-runner/runs.js"; +} from "../agents/embedded-agent-runner/runs.js"; import { getCommandLaneSnapshot, resetCommandLane } from "../process/command-queue.js"; import { getDiagnosticSessionActivitySnapshot } from "./diagnostic-run-activity.js"; import { diagnosticLogger as diag } from "./diagnostic-runtime.js"; @@ -23,21 +23,13 @@ import { import { isDiagnosticSessionStateCurrent } from "./diagnostic-session-state.js"; const STUCK_SESSION_ABORT_SETTLE_MS = 15_000; -// Default no-forward-progress age used only when the caller does not carry a -// resolved `diagnostics.stuckSessionAbortMs`. A run flagged "active" that has made -// no forward progress (tool/model/chunk events) for at least the resolved window, -// while queued work waits, is treated as a leaked/dead handle and reclaimed even -// without an explicit active-abort grant. `lastProgressAgeMs` tracks real progress -// (not incoming queued messages), so it keeps growing while a lane is wedged. const STUCK_SESSION_PROGRESS_STALE_MS = 5 * 60_000; +const recoveriesInFlight = new Set(); + +export type StuckSessionRecoveryParams = StuckSessionRecoveryRequest; function resolveStaleActiveProgressAbortMs(params: StuckSessionRecoveryParams): number { const configured = params.staleActiveProgressAbortMs; - // Honor the resolved `diagnostics.stuckSessionAbortMs` as-is — an operator can - // raise it to protect slow active work (it is the same threshold the existing - // `session.stalled` abort uses). It is floored at the warn threshold upstream, - // not necessarily 5 min, so we only apply the 5-min default when no value is - // carried (e.g. direct callers). return typeof configured === "number" && configured > 0 ? configured : STUCK_SESSION_PROGRESS_STALE_MS; @@ -59,9 +51,6 @@ function isActiveRunProgressStale(params: { const lastProgressAgeMs = activity.lastProgressAgeMs; return typeof lastProgressAgeMs === "number" && lastProgressAgeMs >= params.staleAbortMs; } -const recoveriesInFlight = new Set(); - -export type StuckSessionRecoveryParams = StuckSessionRecoveryRequest; function recoveryKey(params: StuckSessionRecoveryParams): string | undefined { return params.sessionKey?.trim() || params.sessionId?.trim() || undefined; @@ -125,7 +114,7 @@ export async function recoverStuckDiagnosticSession( }; } const fallbackActiveSessionId = - params.sessionId && isEmbeddedPiRunHandleActive(params.sessionId) + params.sessionId && isEmbeddedAgentRunHandleActive(params.sessionId) ? params.sessionId : undefined; const fileActiveSessionId = params.sessionFile @@ -181,7 +170,7 @@ export async function recoverStuckDiagnosticSession( `stuck session recovery reclaiming stale active run: ${formatRecoveryContext(params, { activeSessionId })}`, ); } - const result = await abortAndDrainEmbeddedPiRun({ + const result = await abortAndDrainEmbeddedAgentRun({ sessionId: activeSessionId, sessionKey: params.sessionKey, settleMs: STUCK_SESSION_ABORT_SETTLE_MS, @@ -193,7 +182,7 @@ export async function recoverStuckDiagnosticSession( forceCleared = result.forceCleared; } - if (!activeSessionId && activeWorkSessionId && isEmbeddedPiRunActive(activeWorkSessionId)) { + if (!activeSessionId && activeWorkSessionId && isEmbeddedAgentRunActive(activeWorkSessionId)) { const reclaimStaleReplyWork = params.allowActiveAbort !== true && isActiveRunProgressStale({ @@ -211,7 +200,7 @@ export async function recoverStuckDiagnosticSession( )}`, ); } - const result = await abortAndDrainEmbeddedPiRun({ + const result = await abortAndDrainEmbeddedAgentRun({ sessionId: activeWorkSessionId, sessionKey: params.sessionKey, settleMs: STUCK_SESSION_ABORT_SETTLE_MS, diff --git a/src/mcp/plugin-tools-handlers.ts b/src/mcp/plugin-tools-handlers.ts index 114bcd4e4ab..e31275ff2e4 100644 --- a/src/mcp/plugin-tools-handlers.ts +++ b/src/mcp/plugin-tools-handlers.ts @@ -2,7 +2,7 @@ import { isToolWrappedWithBeforeToolCallHook, rewrapToolWithBeforeToolCallHook, wrapToolWithBeforeToolCallHook, -} from "../agents/pi-tools.before-tool-call.js"; +} from "../agents/agent-tools.before-tool-call.js"; import type { AnyAgentTool } from "../agents/tools/common.js"; import { formatErrorMessage } from "../infra/errors.js"; import { coerceChatContentText } from "../shared/chat-content.js"; diff --git a/src/media-understanding/apply.test.ts b/src/media-understanding/apply.test.ts index 3f2e50ee455..9a332017128 100644 --- a/src/media-understanding/apply.test.ts +++ b/src/media-understanding/apply.test.ts @@ -197,7 +197,6 @@ async function withMediaAutoDetectEnv( GEMINI_API_KEY: undefined, OPENCLAW_ANTIGRAVITY_CLI: undefined, OPENCLAW_AGENT_DIR: undefined, - PI_CODING_AGENT_DIR: undefined, ...env, }, run, @@ -929,7 +928,6 @@ describe("applyMediaUnderstanding", () => { { PATH: emptyBinDir, OPENCLAW_AGENT_DIR: isolatedAgentDir, - PI_CODING_AGENT_DIR: isolatedAgentDir, }, async () => { const result = await applyMediaUnderstanding({ ctx, cfg }); @@ -962,7 +960,6 @@ describe("applyMediaUnderstanding", () => { { PATH: binDir, OPENCLAW_AGENT_DIR: isolatedAgentDir, - PI_CODING_AGENT_DIR: isolatedAgentDir, }, async () => { const result = await applyMediaUnderstanding({ ctx, cfg }); diff --git a/src/media-understanding/image.test.ts b/src/media-understanding/image.test.ts index 3d898ad31b8..ed1ab8fb682 100644 --- a/src/media-understanding/image.test.ts +++ b/src/media-understanding/image.test.ts @@ -76,9 +76,9 @@ function requireRecord(value: unknown, label: string): Record { return value as Record; } -vi.mock("@earendil-works/pi-ai", async () => { +vi.mock("openclaw/plugin-sdk/llm", async () => { const actual = - await vi.importActual("@earendil-works/pi-ai"); + await vi.importActual("openclaw/plugin-sdk/llm"); return { ...actual, complete: completeMock, @@ -102,7 +102,7 @@ vi.mock("../agents/provider-stream.js", () => ({ registerProviderStreamForModel: registerProviderStreamForModelMock, })); -vi.mock("../agents/pi-model-discovery-runtime.js", () => ({ +vi.mock("../agents/agent-model-discovery.js", () => ({ discoverAuthStorage: () => ({ setRuntimeApiKey: setRuntimeApiKeyMock, }), @@ -116,7 +116,7 @@ vi.mock("../plugins/provider-runtime.js", async () => ({ prepareProviderDynamicModel: prepareProviderDynamicModelMock, })); -vi.mock("../agents/pi-embedded-runner/model.js", () => ({ +vi.mock("../agents/embedded-agent-runner/model.js", () => ({ resolveModelAsync: resolveModelAsyncMock, })); @@ -505,7 +505,7 @@ describe("describeImageWithModel", () => { {}, { allowBundledStaticCatalogFallback: true, - skipPiDiscovery: true, + skipAgentDiscovery: true, skipProviderRuntimeHooks: true, workspaceDir: "/tmp/openclaw-workspace", }, @@ -581,7 +581,7 @@ describe("describeImageWithModel", () => { {}, { allowBundledStaticCatalogFallback: true, - skipPiDiscovery: true, + skipAgentDiscovery: true, skipProviderRuntimeHooks: true, }, ); @@ -593,7 +593,7 @@ describe("describeImageWithModel", () => { {}, { allowBundledStaticCatalogFallback: true, - skipPiDiscovery: true, + skipAgentDiscovery: true, }, ); const [completeModel] = requireFirstMockCall(completeMock, "complete"); diff --git a/src/media-understanding/image.ts b/src/media-understanding/image.ts index 6dfbcfcb721..00283cbf7ca 100644 --- a/src/media-understanding/image.ts +++ b/src/media-understanding/image.ts @@ -1,11 +1,11 @@ import type { - Api, AssistantMessage, Context, Model, ProviderStreamOptions, -} from "@earendil-works/pi-ai"; -import { complete } from "@earendil-works/pi-ai"; +} from "openclaw/plugin-sdk/llm"; +import { complete } from "openclaw/plugin-sdk/llm"; +import { resolveModelAsync } from "../agents/embedded-agent-runner/model.js"; import { isMinimaxVlmModel, minimaxUnderstandImage } from "../agents/minimax-vlm.js"; import { getApiKeyForModel, @@ -14,7 +14,6 @@ import { } from "../agents/model-auth.js"; import { normalizeModelRef } from "../agents/model-selection.js"; import { ensureOpenClawModelsJson } from "../agents/models-config.js"; -import { resolveModelAsync } from "../agents/pi-embedded-runner/model.js"; import { resolveProviderRequestCapabilities } from "../agents/provider-attribution.js"; import { registerProviderStreamForModel } from "../agents/provider-stream.js"; import { @@ -27,7 +26,6 @@ import { COPILOT_INTEGRATION_ID, resolveCopilotApiToken, } from "../plugin-sdk/provider-auth.js"; -import { isRecord } from "../shared/record-coerce.js"; import { normalizeMediaProviderId } from "./provider-id.js"; import type { ImageDescriptionRequest, @@ -47,7 +45,11 @@ function resolveImageToolMaxTokens(modelMaxTokens: number | undefined, requested return Math.min(requestedMaxTokens, modelMaxTokens); } -function isNativeResponsesReasoningPayload(model: Model): boolean { +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function isNativeResponsesReasoningPayload(model: Model): boolean { if ( model.api !== "openai-responses" && model.api !== "azure-openai-responses" && @@ -64,7 +66,7 @@ function isNativeResponsesReasoningPayload(model: Model): boolean { }).usesKnownNativeOpenAIRoute; } -function formatModelInputCapabilities(input: Model["input"] | undefined): string { +function formatModelInputCapabilities(input: Model["input"] | undefined): string { return input && input.length > 0 ? input.join(", ") : "none"; } @@ -76,7 +78,7 @@ function removeReasoningInclude(value: unknown): unknown { return next.length > 0 ? next : undefined; } -function disableReasoningForImageRetryPayload(payload: unknown, model: Model): unknown { +function disableReasoningForImageRetryPayload(payload: unknown, model: Model): unknown { if (!isRecord(payload)) { return undefined; } @@ -142,7 +144,7 @@ async function resolveImageRuntime(params: { preferredProfile?: string; authStore?: ImageDescriptionRequest["authStore"]; workspaceDir?: string; -}): Promise<{ apiKey: string; model: Model }> { +}): Promise<{ apiKey: string; model: Model }> { const resolvedRef = normalizeModelRef(params.provider, params.model); const fastResolved = await resolveModelAsync( resolvedRef.provider, @@ -151,7 +153,7 @@ async function resolveImageRuntime(params: { params.cfg, { allowBundledStaticCatalogFallback: true, - skipPiDiscovery: true, + skipAgentDiscovery: true, skipProviderRuntimeHooks: true, ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), }, @@ -164,7 +166,7 @@ async function resolveImageRuntime(params: { params.cfg, { allowBundledStaticCatalogFallback: true, - skipPiDiscovery: true, + skipAgentDiscovery: true, ...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}), }, ); @@ -221,9 +223,9 @@ async function prepareResolvedImageRuntime( authStore?: ImageDescriptionRequest["authStore"]; workspaceDir?: string; }, - resolvedModel: Model, + resolvedModel: Model, authStorage: Awaited>["authStorage"], -): Promise<{ apiKey: string; model: Model }> { +): Promise<{ apiKey: string; model: Model }> { let model = resolvedModel; const apiKeyInfo = await getApiKeyForModel({ model, @@ -278,7 +280,7 @@ function buildImageContext( }; } -function shouldPlaceImagePromptInUserContent(model: Model): boolean { +function shouldPlaceImagePromptInUserContent(model: Model): boolean { // GitHub Copilot models (including Gemini 3.1 Pro Preview) require the // prompt text to be in the user message alongside the image. Placing it // in a separate system message produces "Request must contain at least @@ -299,7 +301,7 @@ function shouldPlaceImagePromptInUserContent(model: Model): boolean { ); } -function buildImageRequestHeaders(model: Model): Record | undefined { +function buildImageRequestHeaders(model: Model): Record | undefined { if (model.provider !== "github-copilot") { return undefined; } @@ -466,7 +468,7 @@ async function describeImagesWithModelInternal( const startedAtMs = Date.now(); const controller = new AbortController(); let apiKey: string; - let model: Model | undefined; + let model: Model | undefined; try { const resolved = await withImageDescriptionTimeout({ diff --git a/src/media-understanding/runner.auto-audio.test.ts b/src/media-understanding/runner.auto-audio.test.ts index 86cc33cad80..4e4c6ca9aad 100644 --- a/src/media-understanding/runner.auto-audio.test.ts +++ b/src/media-understanding/runner.auto-audio.test.ts @@ -321,7 +321,6 @@ describe("runCapability auto audio entries", () => { GOOGLE_API_KEY: undefined, MISTRAL_API_KEY: "mistral-test-key", // pragma: allowlist secret OPENCLAW_AGENT_DIR: isolatedAgentDir, - PI_CODING_AGENT_DIR: isolatedAgentDir, }, async () => { await withAudioFixture("openclaw-auto-audio-mistral", async ({ ctx, media, cache }) => { diff --git a/src/media-understanding/runner.video.test.ts b/src/media-understanding/runner.video.test.ts index 50aa0a13e49..b3e43896f3a 100644 --- a/src/media-understanding/runner.video.test.ts +++ b/src/media-understanding/runner.video.test.ts @@ -114,7 +114,6 @@ describe("runCapability video provider wiring", () => { GOOGLE_API_KEY: undefined, MOONSHOT_API_KEY: undefined, OPENCLAW_AGENT_DIR: isolatedAgentDir, - PI_CODING_AGENT_DIR: isolatedAgentDir, }, async () => { await withVideoFixture("openclaw-video-auto-moonshot", async ({ ctx, media, cache }) => { diff --git a/src/media/read-capability.ts b/src/media/read-capability.ts index fbe3065f57a..7a4d9504133 100644 --- a/src/media/read-capability.ts +++ b/src/media/read-capability.ts @@ -1,7 +1,7 @@ import path from "node:path"; import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js"; +import { resolveGroupToolPolicy } from "../agents/agent-tools.policy.js"; import { resolvePathFromInput } from "../agents/path-policy.js"; -import { resolveGroupToolPolicy } from "../agents/pi-tools.policy.js"; import { resolveEffectiveToolFsRootExpansionAllowed } from "../agents/tool-fs-policy.js"; import { isToolAllowedByPolicies } from "../agents/tool-policy-match.js"; import { resolveWorkspaceRoot } from "../agents/workspace-dir.js"; diff --git a/src/plugin-sdk/agent-core.ts b/src/plugin-sdk/agent-core.ts new file mode 100644 index 00000000000..dc1ce0f0faa --- /dev/null +++ b/src/plugin-sdk/agent-core.ts @@ -0,0 +1 @@ +export * from "../agents/runtime/index.js"; diff --git a/src/plugin-sdk/agent-dir-compat.ts b/src/plugin-sdk/agent-dir-compat.ts index 04957f860c5..ad5a937978a 100644 --- a/src/plugin-sdk/agent-dir-compat.ts +++ b/src/plugin-sdk/agent-dir-compat.ts @@ -6,6 +6,6 @@ import { resolveUserPath } from "../utils.js"; * Kept for third-party plugin SDK compatibility. */ export function resolveOpenClawAgentDir(env: NodeJS.ProcessEnv = process.env): string { - const override = env.OPENCLAW_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim(); + const override = env.OPENCLAW_AGENT_DIR?.trim(); return override ? resolveUserPath(override, env) : resolveDefaultAgentDir({}, env); } diff --git a/src/plugin-sdk/agent-harness-runtime.ts b/src/plugin-sdk/agent-harness-runtime.ts index c66dbf4ed0d..10e7b817049 100644 --- a/src/plugin-sdk/agent-harness-runtime.ts +++ b/src/plugin-sdk/agent-harness-runtime.ts @@ -6,22 +6,22 @@ import type { CodexBundleMcpThreadConfig, LoadCodexBundleMcpThreadConfigParams, } from "../agents/codex-mcp-config.types.js"; -import type { EmbeddedRunAttemptResult } from "../agents/pi-embedded-runner/run/types.js"; +import type { EmbeddedRunAttemptResult } from "../agents/embedded-agent-runner/run/types.js"; import { - abortEmbeddedPiRun, + abortEmbeddedAgentRun, clearActiveEmbeddedRun, - queueEmbeddedPiMessageWithOutcome, + queueEmbeddedAgentMessageWithOutcome, resolveActiveEmbeddedRunSessionId, setActiveEmbeddedRun, - type EmbeddedPiQueueMessageOptions, -} from "../agents/pi-embedded-runner/runs.js"; + type EmbeddedAgentQueueMessageOptions, +} from "../agents/embedded-agent-runner/runs.js"; import { formatToolDetail, resolveToolDisplay } from "../agents/tool-display.js"; import { redactToolDetail } from "../logging/redact.js"; import { truncateUtf16Safe } from "../utils.js"; export const TOOL_PROGRESS_OUTPUT_MAX_CHARS = 8_000; -export type { AgentMessage } from "@earendil-works/pi-agent-core"; +export type { AgentMessage } from "../agents/runtime/index.js"; export type { AgentHarness, AgentHarnessAttemptParams, @@ -39,20 +39,28 @@ export type { export type { EmbeddedRunAttemptParams, EmbeddedRunAttemptResult, -} from "../agents/pi-embedded-runner/run/types.js"; +} from "../agents/embedded-agent-runner/run/types.js"; export type { ContextEngine as HarnessContextEngine, ContextEngineHostCapability, ContextEngineOperation, ContextEngineProjection, } from "../context-engine/types.js"; -export type { CompactEmbeddedPiSessionParams } from "../agents/pi-embedded-runner/compact.js"; -export type { EmbeddedPiCompactResult } from "../agents/pi-embedded-runner/types.js"; +export type { + CompactEmbeddedAgentSessionParams, + /** @deprecated Use CompactEmbeddedAgentSessionParams. */ + CompactEmbeddedAgentSessionParams as CompactEmbeddedPiSessionParams, +} from "../agents/embedded-agent-runner/compact.js"; +export type { + EmbeddedAgentCompactResult, + /** @deprecated Use EmbeddedAgentCompactResult. */ + EmbeddedAgentCompactResult as EmbeddedPiCompactResult, +} from "../agents/embedded-agent-runner/types.js"; export type { AnyAgentTool } from "../agents/tools/common.js"; export type { MessagingToolSend, MessagingToolSourceReplyPayload, -} from "../agents/pi-embedded-messaging.types.js"; +} from "../agents/embedded-agent-messaging.types.js"; export type { HeartbeatToolResponse } from "../auto-reply/heartbeat-tool-response.js"; export type { AgentApprovalEventData, AgentEventPayload } from "../infra/agent-events.js"; export type { ExecApprovalDecision } from "../infra/exec-approvals.js"; @@ -87,10 +95,14 @@ export { formatApprovalDisplayPath } from "../infra/approval-display-paths.js"; export { buildAgentHookContextChannelFields } from "../plugins/hook-agent-context.js"; export { emitAgentEvent, onAgentEvent, resetAgentEventsForTest } from "../infra/agent-events.js"; export { runAgentCleanupStep } from "../agents/run-cleanup-timeout.js"; -export { log as embeddedAgentLog } from "../agents/pi-embedded-runner/logger.js"; +export { log as embeddedAgentLog } from "../agents/embedded-agent-runner/logger.js"; export { buildAgentRuntimePlan } from "../agents/runtime-plan/build.js"; -export { classifyEmbeddedPiRunResultForModelFallback } from "../agents/pi-embedded-runner/result-fallback-classifier.js"; -export { resolveEmbeddedAgentRuntime } from "../agents/pi-embedded-runner/runtime.js"; +export { + classifyEmbeddedAgentRunResultForModelFallback, + /** @deprecated Use classifyEmbeddedAgentRunResultForModelFallback. */ + classifyEmbeddedAgentRunResultForModelFallback as classifyEmbeddedPiRunResultForModelFallback, +} from "../agents/embedded-agent-runner/result-fallback-classifier.js"; +export { resolveEmbeddedAgentRuntime } from "../agents/agent-runtime-id.js"; export { resolveUserPath } from "../utils.js"; export { callGatewayTool } from "../agents/tools/gateway.js"; export type { NodeListNode } from "../agents/tools/nodes-utils.js"; @@ -104,11 +116,11 @@ export { HEARTBEAT_RESPONSE_TOOL_NAME, normalizeHeartbeatToolResponse, } from "../auto-reply/heartbeat-tool-response.js"; -export { isMessagingTool, isMessagingToolSendAction } from "../agents/pi-embedded-messaging.js"; +export { isMessagingTool, isMessagingToolSendAction } from "../agents/embedded-agent-messaging.js"; export { extractToolResultMediaArtifact, filterToolResultMediaUrls, -} from "../agents/pi-embedded-subscribe.tools.js"; +} from "../agents/embedded-agent-subscribe.tools.js"; export { normalizeUsage } from "../agents/usage.js"; export { resolveOpenClawAgentDir } from "./agent-dir-compat.js"; export { @@ -118,10 +130,10 @@ export { } from "../agents/agent-scope.js"; export { resolveModelAuthMode } from "../agents/model-auth.js"; export { supportsModelTools } from "../agents/model-tool-support.js"; -export { resolveAttemptSpawnWorkspaceDir } from "../agents/pi-embedded-runner/run/attempt.thread-helpers.js"; -export { buildEmbeddedAttemptToolRunContext } from "../agents/pi-embedded-runner/run/attempt.tool-run-context.js"; +export { resolveAttemptSpawnWorkspaceDir } from "../agents/embedded-agent-runner/run/attempt.thread-helpers.js"; +export { buildEmbeddedAttemptToolRunContext } from "../agents/embedded-agent-runner/run/attempt.tool-run-context.js"; export { - abortEmbeddedPiRun as abortAgentHarnessRun, + abortEmbeddedAgentRun as abortAgentHarnessRun, clearActiveEmbeddedRun, resolveActiveEmbeddedRunSessionId, setActiveEmbeddedRun, @@ -136,9 +148,9 @@ export { export function queueAgentHarnessMessage( sessionId: string, text: string, - options?: EmbeddedPiQueueMessageOptions, + options?: EmbeddedAgentQueueMessageOptions, ): boolean { - return queueEmbeddedPiMessageWithOutcome(sessionId, text, options).queued; + return queueEmbeddedAgentMessageWithOutcome(sessionId, text, options).queued; } export { disposeRegisteredAgentHarnesses } from "../agents/harness/registry.js"; export { @@ -156,7 +168,7 @@ export type { CodexBundleMcpThreadConfig, LoadCodexBundleMcpThreadConfigParams, } from "../agents/codex-mcp-config.types.js"; -export { normalizeProviderToolSchemas } from "../agents/pi-embedded-runner/tool-schema-runtime.js"; +export { normalizeProviderToolSchemas } from "../agents/embedded-agent-runner/tool-schema-runtime.js"; export async function loadCodexBundleMcpThreadConfig( params: LoadCodexBundleMcpThreadConfigParams, @@ -171,7 +183,7 @@ export { resolveWritableSandboxBindHostRoots, } from "../agents/sandbox/fs-paths.js"; export { resolveBootstrapContextForRun } from "../agents/bootstrap-files.js"; -export type { EmbeddedContextFile } from "../agents/pi-embedded-helpers/types.js"; +export type { EmbeddedContextFile } from "../agents/embedded-agent-helpers/types.js"; export { isSubagentSessionKey } from "../routing/session-key.js"; export { acquireSessionWriteLock, @@ -189,7 +201,7 @@ export { setBeforeToolCallDiagnosticsEnabled, wrapToolWithBeforeToolCallHook, type BeforeToolCallPolicyDiagnosticState, -} from "../agents/pi-tools.before-tool-call.js"; +} from "../agents/agent-tools.before-tool-call.js"; export { resolveAgentHarnessBeforePromptBuildResult, runAgentHarnessAfterCompactionHook, @@ -213,12 +225,12 @@ export { // Plugin-owned (`ownsCompaction`) compaction safety timeout. Exposed on the // agent-harness-runtime surface so plugin harnesses such as Codex bound their // own `ContextEngine.compact()` calls with the exact same finite, host-resolved -// timeout the built-in pi-embedded runner uses — one shared implementation, no +// timeout the built-in embedded-agent runner uses — one shared implementation, no // copy-pasted watchdog. export { compactContextEngineWithSafetyTimeout, resolveCompactionTimeoutMs, -} from "../agents/pi-embedded-runner/compaction-safety-timeout.js"; +} from "../agents/embedded-agent-runner/compaction-safety-timeout.js"; export { estimateRenderedLlmBoundaryTokenPressure, formatPrePromptPrecheckLog, @@ -226,7 +238,7 @@ export { shouldPreemptivelyCompactBeforePrompt, type LlmBoundaryTokenPressure, type PreemptiveCompactionDecision, -} from "../agents/pi-embedded-runner/run/preemptive-compaction.js"; +} from "../agents/embedded-agent-runner/run/preemptive-compaction.js"; export { resolveContextEngineOwnerPluginId } from "../context-engine/registry.js"; export { runAgentHarnessAfterToolCallHook, @@ -248,7 +260,7 @@ export { } from "../agents/harness/native-hook-relay.js"; /** - * Derive the same compact user-facing tool detail that Pi uses for progress logs. + * Derive the same compact user-facing tool detail that embedded OpenClaw uses for progress logs. */ export type ToolProgressDetailMode = "explain" | "raw"; @@ -297,7 +309,7 @@ export type AgentHarnessTerminalOutcomeClassification = NonNullable< * should advance fallback. Deliberate silent replies such as NO_REPLY count as * intentional output, while whitespace-only text remains fallback-eligible. * This is intentionally SDK-level so plugin harness adapters such as Codex - * preserve the same OpenClaw-owned fallback signals as the built-in PI path + * preserve the same OpenClaw-owned fallback signals as the built-in OpenClaw path * without re-implementing terminal-result policy. */ export function classifyAgentHarnessTerminalOutcome( diff --git a/src/plugin-sdk/agent-harness.ts b/src/plugin-sdk/agent-harness.ts index b786e6dcebf..e53301288f9 100644 --- a/src/plugin-sdk/agent-harness.ts +++ b/src/plugin-sdk/agent-harness.ts @@ -2,4 +2,4 @@ // Keep model/vendor-specific protocol code in the plugin that registers the harness. export * from "./agent-harness-runtime.js"; -export { createOpenClawCodingTools } from "../agents/pi-tools.js"; +export { createOpenClawCodingTools } from "../agents/agent-tools.js"; diff --git a/src/plugin-sdk/agent-runtime.ts b/src/plugin-sdk/agent-runtime.ts index 0ed0a2b733d..2e8edc45dff 100644 --- a/src/plugin-sdk/agent-runtime.ts +++ b/src/plugin-sdk/agent-runtime.ts @@ -16,8 +16,8 @@ export * from "../agents/model-catalog.js"; export * from "../agents/model-catalog-scope.js"; export * from "../agents/model-selection.js"; export * from "../agents/simple-completion-runtime.js"; -export * from "../agents/pi-embedded-block-chunker.js"; -export * from "../agents/pi-embedded-utils.js"; +export * from "../agents/embedded-agent-block-chunker.js"; +export * from "../agents/embedded-agent-utils.js"; export * from "../agents/provider-auth-aliases.js"; export * from "../agents/provider-id.js"; export * from "../agents/sandbox-paths.js"; diff --git a/src/plugin-sdk/agent-sessions.ts b/src/plugin-sdk/agent-sessions.ts new file mode 100644 index 00000000000..e5488bc7fca --- /dev/null +++ b/src/plugin-sdk/agent-sessions.ts @@ -0,0 +1 @@ +export * from "../agents/sessions/index.js"; diff --git a/src/plugin-sdk/api-baseline.test.ts b/src/plugin-sdk/api-baseline.test.ts index eeae326ff12..762702074fb 100644 --- a/src/plugin-sdk/api-baseline.test.ts +++ b/src/plugin-sdk/api-baseline.test.ts @@ -5,14 +5,14 @@ import { normalizePluginSdkApiDeclarationText } from "./api-baseline.js"; describe("Plugin SDK API baseline", () => { it("normalizes declaration import paths to repo-relative paths", () => { const repoRoot = process.cwd(); - const modelCatalogPath = path.join(repoRoot, "src", "agents", "pi-model-discovery-runtime"); + const modelCatalogPath = path.join(repoRoot, "src", "agents", "agent-model-discovery"); const declaration = `export function setModelCatalogImportForTest(loader?: (() => Promise) | undefined): void;`; const normalized = normalizePluginSdkApiDeclarationText(repoRoot, declaration); expect(normalized).not.toContain(repoRoot); expect(normalized).toContain( - 'import("src/agents/pi-model-discovery-runtime", { with: { "resolution-mode": "import" } })', + 'import("src/agents/agent-model-discovery", { with: { "resolution-mode": "import" } })', ); }); }); diff --git a/src/plugin-sdk/memory-core-host-runtime-core.ts b/src/plugin-sdk/memory-core-host-runtime-core.ts index 93450933f8c..5e37b8368e5 100644 --- a/src/plugin-sdk/memory-core-host-runtime-core.ts +++ b/src/plugin-sdk/memory-core-host-runtime-core.ts @@ -1,5 +1,9 @@ export * from "../../packages/memory-host-sdk/src/runtime-core.js"; -export { DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR } from "../agents/pi-settings.js"; +export { + DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR, + /** @deprecated Use DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR. */ + DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR as DEFAULT_PI_COMPACTION_RESERVE_TOKENS_FLOOR, +} from "../agents/agent-settings.js"; export { asToolParamsRecord, jsonResult, diff --git a/src/plugin-sdk/memory-core-host-secret.ts b/src/plugin-sdk/memory-core-host-secret.ts index 24828dfda2a..285776b9617 100644 --- a/src/plugin-sdk/memory-core-host-secret.ts +++ b/src/plugin-sdk/memory-core-host-secret.ts @@ -1,4 +1,4 @@ export { hasConfiguredMemorySecretInput, resolveMemorySecretInputString, -} from "../../packages/memory-host-sdk/src/secret.js"; +} from "../memory-host-sdk/secret.js"; diff --git a/src/plugin-sdk/provider-stream-shared.test.ts b/src/plugin-sdk/provider-stream-shared.test.ts index 3f6f466e2ba..2f3e6e1b40c 100644 --- a/src/plugin-sdk/provider-stream-shared.test.ts +++ b/src/plugin-sdk/provider-stream-shared.test.ts @@ -1,68 +1,14 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import { createAssistantMessageEventStream } from "@earendil-works/pi-ai"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; import { createDeepSeekV4OpenAICompatibleThinkingWrapper, createAnthropicThinkingPrefillPayloadWrapper, createPayloadPatchStreamWrapper, - createPlainTextToolCallCompatWrapper, defaultToolStreamExtraParams, isOpenAICompatibleThinkingEnabled, stripTrailingAnthropicAssistantPrefillWhenThinking, } from "./provider-stream-shared.js"; -type StreamEvent = { type: string } & Record; - -function requireRecord(value: unknown, label: string): Record { - if (!value || typeof value !== "object" || Array.isArray(value)) { - throw new Error(`expected ${label} to be a record`); - } - return value as Record; -} - -function createEventStream(events: unknown[]): ReturnType { - const output = createAssistantMessageEventStream(); - const stream = output as unknown as { push(event: unknown): void; end(): void }; - queueMicrotask(() => { - for (const event of events) { - stream.push(event); - } - stream.end(); - }); - return output as ReturnType; -} - -function createControlledPlainTextToolCallCompatStream() { - const source = createAssistantMessageEventStream(); - const baseStream: StreamFn = () => source as ReturnType; - const wrapped = createPlainTextToolCallCompatWrapper(baseStream); - const stream = wrapped( - { provider: "test", api: "openai-completions", id: "test-model" } as never, - { - messages: [], - tools: [{ name: "read", description: "Read", parameters: { type: "object" } }], - } as never, - {}, - ); - return { source, stream }; -} - -async function resolveStream(stream: ReturnType) { - return stream instanceof Promise ? await stream : stream; -} - -async function nextEvent(iterator: AsyncIterator, label: string): Promise { - const result = await Promise.race([ - iterator.next(), - new Promise<"timed out">((resolve) => setTimeout(() => resolve("timed out"), 50)), - ]); - if (result === "timed out") { - throw new Error(`timed out waiting for ${label}`); - } - expect(result.done).toBe(false); - return result.value as StreamEvent; -} - describe("defaultToolStreamExtraParams", () => { it("defaults tool_stream on when absent", () => { expect(defaultToolStreamExtraParams()).toEqual({ tool_stream: true }); @@ -193,165 +139,6 @@ describe("createPayloadPatchStreamWrapper", () => { }); }); -describe("createPlainTextToolCallCompatWrapper", () => { - it("promotes standalone text tool calls into tool-call stream events", async () => { - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_start", content: "" }, - { type: "text_delta", delta: '[tool:read] {"path":"/tmp/file.txt"}' }, - { type: "text_end" }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: '[tool:read] {"path":"/tmp/file.txt"}', - }, - }, - ]); - const wrapped = createPlainTextToolCallCompatWrapper(baseStreamFn); - const events: unknown[] = []; - - for await (const event of wrapped( - {} as never, - { tools: [{ name: "read" }] } as never, - {}, - ) as AsyncIterable) { - events.push(event); - } - - expect(events.map((event) => (event as { type?: string }).type)).toEqual([ - "toolcall_start", - "toolcall_delta", - "done", - ]); - const done = events.at(-1) as { message?: { content?: unknown; stopReason?: unknown } }; - expect(done.message?.stopReason).toBe("toolUse"); - expect(done.message?.content).toEqual([ - expect.objectContaining({ - type: "toolCall", - name: "read", - arguments: { path: "/tmp/file.txt" }, - }), - ]); - }); - - it("passes through bracketed text when no configured tool names match", async () => { - const baseStreamFn: StreamFn = () => - createEventStream([ - { type: "text_delta", delta: "[note] keep streaming" }, - { - type: "done", - reason: "stop", - message: { - role: "assistant", - content: "[note] keep streaming", - }, - }, - ]); - const wrapped = createPlainTextToolCallCompatWrapper(baseStreamFn); - const events: unknown[] = []; - - for await (const event of wrapped( - {} as never, - { tools: [{ name: "read" }] } as never, - {}, - ) as AsyncIterable) { - events.push(event); - } - - expect(events.map((event) => (event as { type?: string }).type)).toEqual([ - "text_delta", - "done", - ]); - }); - - it("converts standalone plain-text tool calls for result consumers", async () => { - const { source, stream } = createControlledPlainTextToolCallCompatStream(); - const resultPromise = (await resolveStream(stream)).result(); - const rawToolText = '[tool:read] {"path":"src/index.ts"}'; - - source.push({ type: "start", partial: { content: [] } } as never); - source.push({ - type: "text_delta", - contentIndex: 0, - delta: rawToolText, - } as never); - source.push({ - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: rawToolText }], - stopReason: "stop", - }, - } as never); - source.end(); - - const message = requireRecord(await resultPromise, "result message"); - expect(message.stopReason).toBe("toolUse"); - expect(requireRecord((message.content as unknown[])[0], "tool call")).toMatchObject({ - type: "toolCall", - name: "read", - arguments: { path: "src/index.ts" }, - }); - }); - - it("keeps CR-separated bracketed tool calls buffered for conversion", async () => { - const { source, stream } = createControlledPlainTextToolCallCompatStream(); - const iterator = (await resolveStream(stream))[Symbol.asyncIterator](); - - try { - source.push({ type: "start", partial: { content: [] } } as never); - expect((await nextEvent(iterator, "start")).type).toBe("start"); - - source.push({ - type: "text_delta", - contentIndex: 0, - delta: '[read]\r{"path":"src/index.ts"}\r[END_TOOL_REQUEST]', - } as never); - source.push({ - type: "done", - reason: "stop", - message: { - role: "assistant", - content: [{ type: "text", text: '[read]\r{"path":"src/index.ts"}\r[END_TOOL_REQUEST]' }], - stopReason: "stop", - }, - } as never); - - const event = await nextEvent(iterator, "converted CR tool call"); - expect(event.type).toBe("toolcall_start"); - } finally { - source.end(); - await iterator.return?.(); - } - }); - - it("does not buffer normal final prose until done", async () => { - const { source, stream } = createControlledPlainTextToolCallCompatStream(); - const iterator = (await resolveStream(stream))[Symbol.asyncIterator](); - - try { - source.push({ type: "start", partial: { content: [] } } as never); - expect((await nextEvent(iterator, "start")).type).toBe("start"); - - source.push({ - type: "text_delta", - contentIndex: 0, - delta: "final answer starts here", - } as never); - - const event = await nextEvent(iterator, "normal final prose"); - expect(event).toMatchObject({ type: "text_delta", delta: "final answer starts here" }); - } finally { - source.push({ type: "done", reason: "stop", message: {} } as never); - source.end(); - await iterator.return?.(); - } - }); -}); - describe("stripTrailingAnthropicAssistantPrefillWhenThinking", () => { it("removes trailing assistant text turns when Anthropic thinking is enabled", () => { const payload = { diff --git a/src/plugin-sdk/provider-stream.test.ts b/src/plugin-sdk/provider-stream.test.ts index 308a6c25f15..82908302d32 100644 --- a/src/plugin-sdk/provider-stream.test.ts +++ b/src/plugin-sdk/provider-stream.test.ts @@ -1,4 +1,4 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it } from "vitest"; import { VERSION } from "../version.js"; import { diff --git a/src/plugin-sdk/provider-usage.test.ts b/src/plugin-sdk/provider-usage.test.ts new file mode 100644 index 00000000000..d0305287c6e --- /dev/null +++ b/src/plugin-sdk/provider-usage.test.ts @@ -0,0 +1,39 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { withTempDir } from "../test-helpers/temp-dir.js"; +import { + resolveLegacyAgentAccessToken, + resolveLegacyPiAgentAccessToken, +} from "./provider-usage.js"; + +async function withLegacyAgentAuthFile( + contents: string, + run: (home: string) => Promise | void, +): Promise { + await withTempDir({ prefix: "openclaw-provider-usage-sdk-" }, async (home) => { + await fs.mkdir(path.join(home, ".pi", "agent"), { recursive: true }); + await fs.writeFile(path.join(home, ".pi", "agent", "auth.json"), contents, "utf8"); + await run(home); + }); +} + +describe("plugin-sdk/provider-usage legacy compatibility", () => { + it.each([ + { + name: "reads legacy agent auth tokens for external plugin compatibility", + contents: `${JSON.stringify({ "z-ai": { access: "legacy-zai-key" } }, null, 2)}\n`, + expected: "legacy-zai-key", + }, + { + name: "returns undefined for invalid legacy agent auth files", + contents: "{not-json", + expected: undefined, + }, + ])("$name", async ({ contents, expected }) => { + await withLegacyAgentAuthFile(contents, async (home) => { + expect(resolveLegacyAgentAccessToken({ HOME: home }, ["z-ai", "zai"])).toBe(expected); + expect(resolveLegacyPiAgentAccessToken({ HOME: home }, ["z-ai", "zai"])).toBe(expected); + }); + }); +}); diff --git a/src/plugin-sdk/provider-usage.ts b/src/plugin-sdk/provider-usage.ts index eb81927d72a..4f1acc55512 100644 --- a/src/plugin-sdk/provider-usage.ts +++ b/src/plugin-sdk/provider-usage.ts @@ -57,3 +57,9 @@ export function resolveLegacyAgentAccessToken( } return undefined; } + +/** + * @deprecated Use `resolveLegacyAgentAccessToken`. Kept only for external + * provider plugins that still import the retired Pi-named SDK helper. + */ +export const resolveLegacyPiAgentAccessToken = resolveLegacyAgentAccessToken; diff --git a/src/plugin-sdk/simple-completion-runtime.ts b/src/plugin-sdk/simple-completion-runtime.ts index d8918aea6c9..ea01a9e3ea2 100644 --- a/src/plugin-sdk/simple-completion-runtime.ts +++ b/src/plugin-sdk/simple-completion-runtime.ts @@ -1,2 +1,2 @@ export * from "../agents/simple-completion-runtime.js"; -export { extractAssistantText } from "../agents/pi-embedded-utils.js"; +export { extractAssistantText } from "../agents/embedded-agent-utils.js"; diff --git a/src/plugin-sdk/test-env.ts b/src/plugin-sdk/test-env.ts index f743c23b668..4610e93484c 100644 --- a/src/plugin-sdk/test-env.ts +++ b/src/plugin-sdk/test-env.ts @@ -19,7 +19,7 @@ export { isOverloadedErrorMessage, isServerErrorMessage, isTimeoutErrorMessage, -} from "../agents/pi-embedded-helpers/failover-matches.js"; +} from "../agents/embedded-agent-helpers/failover-matches.js"; export { maybeLoadShellEnvForGenerationProviders } from "../test-utils/generation-live-test-helpers.js"; export { isTruthyEnvValue } from "../infra/env.js"; export { getShellEnvAppliedKeys } from "../infra/shell-env.js"; diff --git a/src/plugin-sdk/test-helpers/agents/openclaw-owned-tool-runtime-contract.ts b/src/plugin-sdk/test-helpers/agents/openclaw-owned-tool-runtime-contract.ts index 1fd99558b27..06a9caee5da 100644 --- a/src/plugin-sdk/test-helpers/agents/openclaw-owned-tool-runtime-contract.ts +++ b/src/plugin-sdk/test-helpers/agents/openclaw-owned-tool-runtime-contract.ts @@ -1,6 +1,6 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; import { vi } from "vitest"; -import { resetAdjustedParamsByToolCallIdForTests } from "../../../agents/pi-tools.before-tool-call.state.js"; +import { resetAdjustedParamsByToolCallIdForTests } from "../../../agents/agent-tools.before-tool-call.state.js"; +import type { AgentToolResult } from "../../../agents/runtime/index.js"; import type { CodexAppServerExtensionFactory, CodexAppServerToolResultEvent, diff --git a/src/plugin-sdk/test-helpers/agents/outcome-fallback-runtime-contract.ts b/src/plugin-sdk/test-helpers/agents/outcome-fallback-runtime-contract.ts index b9be03c09cc..e958226f381 100644 --- a/src/plugin-sdk/test-helpers/agents/outcome-fallback-runtime-contract.ts +++ b/src/plugin-sdk/test-helpers/agents/outcome-fallback-runtime-contract.ts @@ -1,4 +1,4 @@ -import type { EmbeddedPiRunResult } from "../../../agents/pi-embedded-runner/types.js"; +import type { EmbeddedAgentRunResult } from "../../../agents/embedded-agent-runner/types.js"; export const OUTCOME_FALLBACK_RUNTIME_CONTRACT = { primaryProvider: "openai-codex", @@ -14,8 +14,8 @@ export const OUTCOME_FALLBACK_RUNTIME_CONTRACT = { } as const; export function createContractRunResult( - overrides: Partial = {}, -): EmbeddedPiRunResult { + overrides: Partial = {}, +): EmbeddedAgentRunResult { const { meta, ...rest } = overrides; return { payloads: [], diff --git a/src/plugin-sdk/test-helpers/agents/transcript-repair-runtime-contract.ts b/src/plugin-sdk/test-helpers/agents/transcript-repair-runtime-contract.ts index 65111854b66..6d014032224 100644 --- a/src/plugin-sdk/test-helpers/agents/transcript-repair-runtime-contract.ts +++ b/src/plugin-sdk/test-helpers/agents/transcript-repair-runtime-contract.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../../../agents/runtime/index.js"; export const QUEUED_USER_MESSAGE_MARKER = "[Queued user message that arrived while the previous turn was still active]"; diff --git a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts index a4e9e4707c7..fec771be410 100644 --- a/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts +++ b/src/plugin-sdk/test-helpers/plugin-runtime-mock.ts @@ -139,6 +139,10 @@ export function createPluginRuntimeMediaMock( } export function createPluginRuntimeMock(overrides: DeepPartial = {}): PluginRuntime { + const runEmbeddedAgentMock = vi.fn().mockResolvedValue({ + payloads: [], + meta: {}, + }) as unknown as PluginRuntime["agent"]["runEmbeddedAgent"]; const taskFlow = { bindSession: vi.fn( createTaskFlowSessionMock, @@ -401,14 +405,8 @@ export function createPluginRuntimeMock(overrides: DeepPartial = { id: "high", label: "high" }, ], })) as unknown as PluginRuntime["agent"]["resolveThinkingPolicy"], - runEmbeddedPiAgent: vi.fn().mockResolvedValue({ - payloads: [], - meta: {}, - }) as unknown as PluginRuntime["agent"]["runEmbeddedPiAgent"], - runEmbeddedAgent: vi.fn().mockResolvedValue({ - payloads: [], - meta: {}, - }) as unknown as PluginRuntime["agent"]["runEmbeddedAgent"], + runEmbeddedAgent: runEmbeddedAgentMock, + runEmbeddedPiAgent: runEmbeddedAgentMock, resolveAgentTimeoutMs: vi.fn( () => 30_000, ) as unknown as PluginRuntime["agent"]["resolveAgentTimeoutMs"], diff --git a/src/plugin-sdk/test-helpers/provider-runtime-contract.ts b/src/plugin-sdk/test-helpers/provider-runtime-contract.ts index 32a699a55f3..388957a15c9 100644 --- a/src/plugin-sdk/test-helpers/provider-runtime-contract.ts +++ b/src/plugin-sdk/test-helpers/provider-runtime-contract.ts @@ -1,6 +1,3 @@ -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ProviderRuntimeModel } from "../plugin-entry.js"; import { registerProviderPlugin, requireRegisteredProvider } from "../plugin-test-runtime.js"; @@ -9,7 +6,7 @@ import { createProviderUsageFetch, makeResponse } from "../test-env.js"; const CONTRACT_SETUP_TIMEOUT_MS = 300_000; -const OAUTH_MODULE_ID = "@earendil-works/pi-ai/oauth"; +const OAUTH_MODULE_ID = "openclaw/plugin-sdk/llm-oauth"; const OPENAI_CODEX_PROVIDER_RUNTIME_MODULE_ID = "../../../extensions/openai/openai-codex-provider.runtime.js"; const refreshOpenAICodexTokenMock = vi.fn(); @@ -22,7 +19,7 @@ const getOAuthProvidersMock = vi.fn(() => [ function installProviderRuntimeContractMocks() { vi.doMock(OAUTH_MODULE_ID, async () => { const actual = - await vi.importActual(OAUTH_MODULE_ID); + await vi.importActual(OAUTH_MODULE_ID); return { ...actual, refreshOpenAICodexToken: refreshOpenAICodexTokenMock, @@ -468,7 +465,7 @@ export function describeOpenAIProviderRuntimeContract(load: ProviderRuntimeContr }); }); - it("leaves openai gpt-5.5 forward-compat resolution to Pi", () => { + it("leaves openai gpt-5.5 forward-compat resolution to OpenClaw", () => { const provider = requireProviderContractProvider("openai"); const model = provider.resolveDynamicModel?.({ provider: "openai", @@ -587,7 +584,7 @@ export function describeOpenAIProviderRuntimeContract(load: ProviderRuntimeContr }); }); - it("keeps Pi cost metadata but applies Codex context metadata for gpt-5.5 models", () => { + it("keeps OpenClaw cost metadata but applies Codex context metadata for gpt-5.5 models", () => { const provider = requireProviderContractProvider("openai-codex"); const model = provider.resolveDynamicModel?.({ provider: "openai-codex", @@ -810,33 +807,6 @@ export function describeZAIProviderRuntimeContract(load: ProviderRuntimeContract }); }); - it("falls back to legacy pi auth tokens for usage auth", async () => { - const provider = requireProviderContractProvider("zai"); - const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-zai-contract-")); - await fs.mkdir(path.join(home, ".pi", "agent"), { recursive: true }); - await fs.writeFile( - path.join(home, ".pi", "agent", "auth.json"), - `${JSON.stringify({ "z-ai": { access: "legacy-zai-token" } }, null, 2)}\n`, - "utf8", - ); - - try { - await expect( - provider.resolveUsageAuth?.({ - config: {} as never, - env: { HOME: home } as NodeJS.ProcessEnv, - provider: "zai", - resolveApiKeyFromConfigAndStore: () => undefined, - resolveOAuthToken: async () => null, - }), - ).resolves.toEqual({ - token: "legacy-zai-token", - }); - } finally { - await fs.rm(home, { recursive: true, force: true }); - } - }); - it("owns usage snapshot fetching", async () => { const provider = requireProviderContractProvider("zai"); const mockFetch = createProviderUsageFetch(async (url) => { diff --git a/src/plugin-sdk/test-helpers/stream-hooks.ts b/src/plugin-sdk/test-helpers/stream-hooks.ts index 096ad65a704..b3b3154d036 100644 --- a/src/plugin-sdk/test-helpers/stream-hooks.ts +++ b/src/plugin-sdk/test-helpers/stream-hooks.ts @@ -1,4 +1,4 @@ -import type { StreamFn } from "@earendil-works/pi-agent-core"; +import type { StreamFn } from "../../agents/runtime/index.js"; export function createCapturedThinkingConfigStream() { let capturedPayload: Record | undefined; diff --git a/src/plugin-sdk/testing.ts b/src/plugin-sdk/testing.ts index cabd85a06a7..4b2e71d9e6a 100644 --- a/src/plugin-sdk/testing.ts +++ b/src/plugin-sdk/testing.ts @@ -121,7 +121,7 @@ export { isOverloadedErrorMessage, isServerErrorMessage, isTimeoutErrorMessage, -} from "../agents/pi-embedded-helpers/failover-matches.js"; +} from "../agents/embedded-agent-helpers/failover-matches.js"; export { maybeLoadShellEnvForGenerationProviders } from "../test-utils/generation-live-test-helpers.js"; export { testing, testing as __testing } from "../acp/control-plane/manager.js"; export { testing as acpManagerTesting } from "../acp/control-plane/manager.js"; diff --git a/src/plugin-sdk/tool-plugin.ts b/src/plugin-sdk/tool-plugin.ts index 11901b22e2c..0d51e748926 100644 --- a/src/plugin-sdk/tool-plugin.ts +++ b/src/plugin-sdk/tool-plugin.ts @@ -1,5 +1,5 @@ -import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core"; import { Type, type Static, type TSchema } from "typebox"; +import type { AgentToolResult, AgentToolUpdateCallback } from "../agents/runtime/index.js"; import { jsonResult, textResult } from "../agents/tools/common.js"; import type { PluginManifestActivation } from "../plugins/manifest.js"; import type { JsonSchemaObject } from "../shared/json-schema.types.js"; @@ -19,7 +19,7 @@ export type ToolPluginExecutionContext = { api: OpenClawPluginApi; signal?: AbortSignal; toolCallId: string; - onUpdate?: AgentToolUpdateCallback; + onUpdate?: AgentToolUpdateCallback; }; type ToolPluginConfig = TConfigSchema extends TSchema diff --git a/src/plugins/agent-tool-result-middleware-types.ts b/src/plugins/agent-tool-result-middleware-types.ts index b355dc08606..30f2e2fbb6e 100644 --- a/src/plugins/agent-tool-result-middleware-types.ts +++ b/src/plugins/agent-tool-result-middleware-types.ts @@ -1,11 +1,14 @@ -import type { AgentToolResult as PiAgentToolResult } from "@earendil-works/pi-agent-core"; +import type { AgentToolResult } from "../agents/runtime/index.js"; -export type OpenClawAgentToolResult = PiAgentToolResult; +export type OpenClawAgentToolResult = AgentToolResult; -export type AgentToolResultMiddlewareRuntime = "pi" | "codex"; +export type AgentToolResultMiddlewareRuntime = "openclaw" | "codex"; +/** @deprecated Use "openclaw". */ +export type AgentToolResultMiddlewareLegacyRuntime = "pi"; /** @deprecated Use AgentToolResultMiddlewareRuntime. */ export type AgentToolResultMiddlewareHarness = | AgentToolResultMiddlewareRuntime + | AgentToolResultMiddlewareLegacyRuntime | "codex-app-server"; export type AgentToolResultMiddlewareEvent = { @@ -39,7 +42,7 @@ export type AgentToolResultMiddleware = ( ) => Promise | AgentToolResultMiddlewareResult | void; export type AgentToolResultMiddlewareOptions = { - runtimes?: AgentToolResultMiddlewareRuntime[]; + runtimes?: Array; /** @deprecated Use runtimes. */ harnesses?: AgentToolResultMiddlewareHarness[]; }; diff --git a/src/plugins/agent-tool-result-middleware.test.ts b/src/plugins/agent-tool-result-middleware.test.ts index c01b7f4cc22..51ec5dd5cfd 100644 --- a/src/plugins/agent-tool-result-middleware.test.ts +++ b/src/plugins/agent-tool-result-middleware.test.ts @@ -3,7 +3,7 @@ import { normalizeAgentToolResultMiddlewareRuntimes } from "./agent-tool-result- describe("normalizeAgentToolResultMiddlewareRuntimes", () => { it("defaults omitted runtimes to every supported runtime", () => { - expect(normalizeAgentToolResultMiddlewareRuntimes()).toEqual(["pi", "codex"]); + expect(normalizeAgentToolResultMiddlewareRuntimes()).toEqual(["openclaw", "codex"]); }); it("preserves an explicit empty runtime list", () => { @@ -12,8 +12,15 @@ describe("normalizeAgentToolResultMiddlewareRuntimes", () => { it("normalizes legacy harness names", () => { expect( - normalizeAgentToolResultMiddlewareRuntimes({ harnesses: ["codex-app-server", "pi"] }), - ).toEqual(["codex", "pi"]); + normalizeAgentToolResultMiddlewareRuntimes({ harnesses: ["codex-app-server", "openclaw"] }), + ).toEqual(["codex", "openclaw"]); + }); + + it("normalizes legacy runtime names to openclaw", () => { + expect(normalizeAgentToolResultMiddlewareRuntimes({ runtimes: ["pi", "codex"] })).toEqual([ + "openclaw", + "codex", + ]); }); it("falls back to legacy harnesses when runtimes is undefined", () => { diff --git a/src/plugins/agent-tool-result-middleware.ts b/src/plugins/agent-tool-result-middleware.ts index fc1212631ba..66cdc1c5990 100644 --- a/src/plugins/agent-tool-result-middleware.ts +++ b/src/plugins/agent-tool-result-middleware.ts @@ -6,7 +6,7 @@ import type { import { getActivePluginRegistry } from "./runtime.js"; export const AGENT_TOOL_RESULT_MIDDLEWARE_RUNTIMES = [ - "pi", + "openclaw", "codex", ] as const satisfies AgentToolResultMiddlewareRuntime[]; @@ -14,12 +14,21 @@ const AGENT_TOOL_RESULT_MIDDLEWARE_RUNTIME_SET = new Set( AGENT_TOOL_RESULT_MIDDLEWARE_RUNTIMES, ); +const LEGACY_AGENT_TOOL_RESULT_MIDDLEWARE_RUNTIMES = { + "codex-app-server": "codex", + pi: "openclaw", +} as const satisfies Record; + function normalizeAgentToolResultMiddlewareRuntime( runtime: string, ): AgentToolResultMiddlewareRuntime | undefined { const normalized = runtime.trim().toLowerCase(); - if (normalized === "codex-app-server") { - return "codex"; + const legacyRuntime = + LEGACY_AGENT_TOOL_RESULT_MIDDLEWARE_RUNTIMES[ + normalized as keyof typeof LEGACY_AGENT_TOOL_RESULT_MIDDLEWARE_RUNTIMES + ]; + if (legacyRuntime) { + return legacyRuntime; } return AGENT_TOOL_RESULT_MIDDLEWARE_RUNTIME_SET.has(normalized) ? (normalized as AgentToolResultMiddlewareRuntime) diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index ad1e53d916e..a549f66f35c 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -183,6 +183,14 @@ function createManifestRegistryFixture(): PluginManifestRegistry { musicGenerationProviders: ["google"], }, }, + { + id: "amazon-bedrock", + channels: [], + origin: "bundled", + enabledByDefault: true, + providers: ["amazon-bedrock"], + cliBackends: [], + }, { id: "brave", channels: [], @@ -685,6 +693,25 @@ describe("resolveGatewayStartupPluginIds", () => { }), ["demo-channel", "browser", "memory-core"], ], + [ + "includes bundled model providers selected by agent defaults at startup", + createStartupConfig({ + modelId: "amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0", + }), + ["demo-channel", "browser", "amazon-bedrock", "memory-core"], + ], + [ + "honors explicit plugin disablement for selected model providers", + { + agents: { + defaults: { + model: { primary: "amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0" }, + }, + }, + plugins: { entries: { "amazon-bedrock": { enabled: false } } }, + } as OpenClawConfig, + ["demo-channel", "browser", "memory-core"], + ], [ "includes configured bundled speech providers at startup", { @@ -1618,14 +1645,14 @@ describe("resolveGatewayStartupPluginIds", () => { }); }); - it("does not include Codex when an OpenAI model is manually pinned to PI", () => { + it("does not include Codex when an OpenAI model is manually pinned to OpenClaw", () => { expectStartupPluginIdsCase({ config: { agents: { defaults: { model: { primary: "openai/gpt-5.5" }, models: { - "openai/gpt-5.5": { agentRuntime: { id: "pi" } }, + "openai/gpt-5.5": { agentRuntime: { id: "openclaw" } }, }, }, }, diff --git a/src/plugins/codex-app-server-extension-types.ts b/src/plugins/codex-app-server-extension-types.ts index c9dd43677f0..492094e4ba5 100644 --- a/src/plugins/codex-app-server-extension-types.ts +++ b/src/plugins/codex-app-server-extension-types.ts @@ -1,4 +1,4 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; +import type { AgentToolResult } from "../agents/runtime/index.js"; export type CodexAppServerToolResultEvent = { threadId: string; diff --git a/src/plugins/command-registration.ts b/src/plugins/command-registration.ts index 6d5d34e03e2..cac03080c18 100644 --- a/src/plugins/command-registration.ts +++ b/src/plugins/command-registration.ts @@ -75,6 +75,11 @@ function getAgentPromptSurfaces(): Set { return agentPromptSurfaces; } +function normalizeAgentPromptSurface(value: string): AgentPromptSurfaceKind { + const surface = value.trim(); + return (surface === "pi_main" ? "openclaw_main" : surface) as AgentPromptSurfaceKind; +} + export type CommandRegistrationResult = { ok: boolean; error?: string; @@ -263,9 +268,7 @@ function normalizeAgentPromptGuidance( text: entry.text.trim(), }; if (entry.surfaces) { - normalized.surfaces = entry.surfaces.map( - (surface) => surface.trim() as AgentPromptSurfaceKind, - ); + normalized.surfaces = entry.surfaces.map(normalizeAgentPromptSurface); } return normalized; }); diff --git a/src/plugins/command-registry-state.ts b/src/plugins/command-registry-state.ts index 7b978c1aa99..6d3976b3275 100644 --- a/src/plugins/command-registry-state.ts +++ b/src/plugins/command-registry-state.ts @@ -71,7 +71,7 @@ export function listRegisteredPluginAgentPromptGuidance(params?: { for (const command of pluginCommands.values()) { for (const entry of command.agentPromptGuidance ?? []) { const trimmed = resolveAgentPromptGuidanceTextForSurface(entry, { - surface: params?.surface, + surface: normalizeAgentPromptSurface(params?.surface), includeLegacyGlobalGuidance: params?.includeLegacyGlobalGuidance ?? true, }); if (!trimmed || seen.has(trimmed)) { @@ -84,6 +84,12 @@ export function listRegisteredPluginAgentPromptGuidance(params?: { return lines; } +function normalizeAgentPromptSurface( + surface: AgentPromptSurfaceKind | undefined, +): AgentPromptSurfaceKind | undefined { + return surface === "pi_main" ? "openclaw_main" : surface; +} + function resolveAgentPromptGuidanceTextForSurface( entry: AgentPromptGuidance, params: { diff --git a/src/plugins/commands.test.ts b/src/plugins/commands.test.ts index 28df6609889..e8fb4ab5a83 100644 --- a/src/plugins/commands.test.ts +++ b/src/plugins/commands.test.ts @@ -385,7 +385,7 @@ describe("registerPluginCommand", () => { expected: { ok: false, error: - "Agent prompt guidance 1 surface 1 must be one of: pi_main, codex_app_server, cli_backend, acp_backend, subagent", + "Agent prompt guidance 1 surface 1 must be one of: openclaw_main, pi_main, codex_app_server, cli_backend, acp_backend, subagent", }, }, { @@ -465,7 +465,7 @@ describe("registerPluginCommand", () => { " Use /demo_cmd everywhere. ", { text: " Use /demo_cmd for main agent routing. ", - surfaces: ["pi_main"], + surfaces: ["openclaw_main"], }, { text: "Use /demo_cmd for subagents.", @@ -481,6 +481,10 @@ describe("registerPluginCommand", () => { "Use /demo_cmd for main agent routing.", "Use /demo_cmd for subagents.", ]); + expect(listRegisteredPluginAgentPromptGuidance({ surface: "openclaw_main" })).toEqual([ + "Use /demo_cmd everywhere.", + "Use /demo_cmd for main agent routing.", + ]); expect(listRegisteredPluginAgentPromptGuidance({ surface: "pi_main" })).toEqual([ "Use /demo_cmd everywhere.", "Use /demo_cmd for main agent routing.", diff --git a/src/plugins/compat/registry.test.ts b/src/plugins/compat/registry.test.ts index 50c237a7891..57d4e7daec8 100644 --- a/src/plugins/compat/registry.test.ts +++ b/src/plugins/compat/registry.test.ts @@ -26,6 +26,23 @@ const deprecatedTargetParserCompatFiles = new Set([ "src/plugins/compat/registry.test.ts", ]); +function listTsFiles(root: string): string[] { + const results: string[] = []; + for (const entry of fs.readdirSync(root, { withFileTypes: true })) { + const childPath = `${root}/${entry.name}`; + if (entry.isDirectory()) { + if (entry.name !== "node_modules") { + results.push(...listTsFiles(childPath)); + } + continue; + } + if (/\.(?:ts|tsx)$/u.test(entry.name)) { + results.push(childPath); + } + } + return results; +} + const knownDeprecatedSurfaceMarkers = [ { code: "legacy-extension-api-import", @@ -62,6 +79,11 @@ const knownDeprecatedSurfaceMarkers = [ file: "src/plugins/agent-tool-result-middleware-types.ts", marker: "AgentToolResultMiddlewareHarness", }, + { + code: "embedded-pi-agent-sdk-aliases", + file: "src/plugins/runtime/types-core.ts", + marker: "runEmbeddedPiAgent", + }, { code: "runtime-config-load-write", file: "src/plugins/runtime/runtime-config.ts", diff --git a/src/plugins/compat/registry.ts b/src/plugins/compat/registry.ts index bb233ca4356..2f79dfc08f0 100644 --- a/src/plugins/compat/registry.ts +++ b/src/plugins/compat/registry.ts @@ -78,7 +78,7 @@ export const PLUGIN_COMPAT_RECORDS = [ diagnostics: ["hook runner contract probe"], tests: [ "src/plugins/hooks.security.test.ts", - "src/agents/pi-tools.before-tool-call.e2e.test.ts", + "src/agents/agent-tools.before-tool-call.e2e.test.ts", ], }, { @@ -430,6 +430,29 @@ export const PLUGIN_COMPAT_RECORDS = [ diagnostics: ["plugin SDK compatibility warning"], tests: ["src/plugins/contracts/plugin-sdk-subpaths.test.ts"], }, + { + code: "embedded-pi-agent-sdk-aliases", + status: "deprecated", + owner: "agent-runtime", + introduced: "2026-05-21", + deprecated: "2026-05-21", + warningStarts: "2026-05-21", + removeAfter: "2026-08-21", + replacement: "`runEmbeddedAgent` and `EmbeddedAgent*` SDK/runtime names", + docsPath: "/plugins/sdk-runtime", + surfaces: [ + "api.runtime.agent.runEmbeddedPiAgent", + "openclaw/extension-api runEmbeddedPiAgent", + "openclaw/plugin-sdk/agent-harness-runtime EmbeddedPi* aliases", + ], + diagnostics: ["plugin SDK compatibility registry"], + tests: [ + "src/plugins/runtime/index.test.ts", + "src/plugins/contracts/plugin-sdk-subpaths.test.ts", + ], + releaseNote: + "Legacy `runEmbeddedPiAgent` and `EmbeddedPi*` plugin aliases remain as deprecated SDK compatibility only.", + }, { code: "agent-harness-id-alias", status: "deprecated", diff --git a/src/plugins/contracts/tts-contract-suites.ts b/src/plugins/contracts/tts-contract-suites.ts index e651ad9a45b..0b1c8d19b93 100644 --- a/src/plugins/contracts/tts-contract-suites.ts +++ b/src/plugins/contracts/tts-contract-suites.ts @@ -1,5 +1,5 @@ -import type { AssistantMessage } from "@earendil-works/pi-ai"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { createEmptyPluginRegistry, pluginRegistrationContractRegistry, @@ -23,7 +23,7 @@ let ttsRuntime: TtsRuntimeModule; let ttsRuntimePromise: Promise | null = null; let ttsRuntimeInitialized = false; let ttsCorePromise: Promise | null = null; -let completeSimple: typeof import("@earendil-works/pi-ai").completeSimple; +let completeSimple: typeof import("openclaw/plugin-sdk/llm").completeSimple; let getApiKeyForModelMock: SummarizeTextDeps["getApiKeyForModel"]; let requireApiKeyMock: SummarizeTextDeps["requireApiKey"]; let resolveModelAsyncMock: SummarizeTextDeps["resolveModelAsync"]; @@ -70,12 +70,9 @@ async function withIsolatedSpeechProviderEnvAsync( return await withEnvAsync(isolatedSpeechProviderEnv(overrides), fn); } -vi.mock("@earendil-works/pi-ai", async () => { - const actual = - await vi.importActual("@earendil-works/pi-ai"); +vi.mock("openclaw/plugin-sdk/llm", () => { const getApiProvider = vi.fn(() => undefined); return { - ...actual, completeSimple: vi.fn(), createAssistantMessageEventStream: vi.fn(), getApiProvider, @@ -87,7 +84,7 @@ vi.mock("@earendil-works/pi-ai", async () => { }; }); -vi.mock("@earendil-works/pi-ai/oauth", () => { +vi.mock("openclaw/plugin-sdk/llm-oauth", () => { return { getOAuthProviders: () => [], getOAuthApiKey: vi.fn(async () => null), @@ -507,7 +504,7 @@ function createResolvedSummarizationConfig(cfg: OpenClawConfig): ResolvedTtsConf async function setupSummarizationMocks() { ({ summarizeText: summarizeTextCore } = await loadTtsCore()); - ({ completeSimple } = await import("@earendil-works/pi-ai")); + ({ completeSimple } = await import("openclaw/plugin-sdk/llm")); getApiKeyForModelMock = vi.fn() as SummarizeTextDeps["getApiKeyForModel"]; requireApiKeyMock = vi.fn() as SummarizeTextDeps["requireApiKey"]; resolveModelAsyncMock = vi.fn() as SummarizeTextDeps["resolveModelAsync"]; diff --git a/src/plugins/gateway-startup-plugin-ids.ts b/src/plugins/gateway-startup-plugin-ids.ts index 7798b9a7bc2..179f5c6b1fa 100644 --- a/src/plugins/gateway-startup-plugin-ids.ts +++ b/src/plugins/gateway-startup-plugin-ids.ts @@ -1,9 +1,11 @@ import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js"; +import { normalizeProviderId } from "../agents/provider-id.js"; import { listExplicitlyDisabledChannelIdsForConfig, listPotentialConfiguredChannelIds, } from "../channels/config-presence.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { MODELS } from "../llm/models.generated.js"; import { DEFAULT_MEMORY_DREAMING_PLUGIN_ID, resolveMemoryDreamingConfig, @@ -45,6 +47,16 @@ type GenerationProviderContractKey = | "videoGenerationProviders" | "musicGenerationProviders"; type ConfiguredGenerationProviderIds = Record>; +const CORE_BUILT_IN_MODEL_APIS = new Set([ + "anthropic-messages", + "azure-openai-responses", + "google-generative-ai", + "google-vertex", + "mistral-conversations", + "openai-codex-responses", + "openai-completions", + "openai-responses", +]); function isConfigActivationValueEnabled(value: unknown): boolean { if (value === false) { @@ -254,17 +266,104 @@ function listModelProviderRefs(value: unknown): string[] { return refs; } +function listModelProviderRefParts(value: unknown): Array<{ providerId: string; modelId: string }> { + return listModelProviderRefs(value) + .map((ref) => { + const slashIndex = ref.indexOf("/"); + if (slashIndex <= 0 || slashIndex >= ref.length - 1) { + return undefined; + } + return { + providerId: normalizeProviderId(ref.slice(0, slashIndex)), + modelId: ref.slice(slashIndex + 1).trim(), + }; + }) + .filter((entry): entry is { providerId: string; modelId: string } => + Boolean(entry?.providerId && entry.modelId), + ); +} + function collectModelProviderIds(value: unknown): ReadonlySet { return new Set( listModelProviderRefs(value) .map((ref) => { const slashIndex = ref.indexOf("/"); - return slashIndex > 0 ? normalizeOptionalLowercaseString(ref.slice(0, slashIndex)) : ""; + return slashIndex > 0 ? normalizeProviderId(ref.slice(0, slashIndex)) : ""; }) .filter((providerId): providerId is string => Boolean(providerId)), ); } +function collectConfiguredAgentModelProviderIds(config: OpenClawConfig): ReadonlySet { + const modelIdsByProvider = new Map>(); + const addModelProviderRefs = (value: unknown) => { + for (const { providerId, modelId } of listModelProviderRefParts(value)) { + const modelIds = modelIdsByProvider.get(providerId) ?? new Set(); + modelIds.add(modelId); + modelIdsByProvider.set(providerId, modelIds); + } + }; + const addModelMapProviderIds = (models: unknown) => { + if (!isRecord(models)) { + return; + } + for (const modelRef of Object.keys(models)) { + addModelProviderRefs(modelRef); + } + }; + + const defaults = config.agents?.defaults; + addModelProviderRefs(defaults?.model); + addModelMapProviderIds(defaults?.models); + + const agents = Array.isArray(config.agents?.list) ? config.agents.list : []; + for (const agent of agents) { + if (!isRecord(agent)) { + continue; + } + addModelProviderRefs(agent.model); + addModelMapProviderIds(agent.models); + } + + return new Set( + [...modelIdsByProvider.entries()] + .filter(([providerId, modelIds]) => { + return [...modelIds].some((modelId) => + configuredModelProviderNeedsRuntimePlugin({ config, providerId, modelId }), + ); + }) + .map(([providerId]) => providerId), + ); +} + +function configuredModelProviderNeedsRuntimePlugin(params: { + config: OpenClawConfig; + providerId: string; + modelId: string; +}): boolean { + const providerConfig = params.config.models?.providers?.[params.providerId]; + const configuredModel = providerConfig?.models?.find((model) => model.id === params.modelId); + const modelApi = + configuredModel?.api ?? + providerConfig?.api ?? + (MODELS as Record | undefined>)[params.providerId]?.[ + params.modelId + ]?.api; + return typeof modelApi === "string" && !CORE_BUILT_IN_MODEL_APIS.has(modelApi); +} + +function manifestOwnsConfiguredModelProvider(params: { + manifest: PluginManifestRecord | undefined; + configuredModelProviderIds: ReadonlySet; +}): boolean { + if (params.configuredModelProviderIds.size === 0) { + return false; + } + return (params.manifest?.providers ?? []).some((providerId) => { + return params.configuredModelProviderIds.has(normalizeProviderId(providerId)); + }); +} + function collectConfiguredGenerationProviderIds( config: OpenClawConfig, ): ConfiguredGenerationProviderIds { @@ -350,6 +449,55 @@ function canStartConfiguredGenerationProviderPlugin(params: { ); } +function canStartConfiguredModelProviderPlugin(params: { + plugin: InstalledPluginIndexRecord; + manifest: PluginManifestRecord | undefined; + config: OpenClawConfig; + pluginsConfig: ReturnType; + activationSource: { + plugins: ReturnType; + rootConfig?: OpenClawConfig; + }; + configuredModelProviderIds: ReadonlySet; + platform?: NodeJS.Platform; +}): boolean { + if ( + !manifestOwnsConfiguredModelProvider({ + manifest: params.manifest, + configuredModelProviderIds: params.configuredModelProviderIds, + }) + ) { + return false; + } + if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) { + return false; + } + if ( + params.pluginsConfig.deny.includes(params.plugin.pluginId) || + params.activationSource.plugins.deny.includes(params.plugin.pluginId) + ) { + return false; + } + if ( + params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false || + params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false + ) { + return false; + } + const activationState = resolveEffectivePluginActivationState({ + id: params.plugin.pluginId, + origin: params.plugin.origin, + config: params.pluginsConfig, + rootConfig: params.config, + enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform), + activationSource: params.activationSource, + }); + return ( + activationState.enabled && + (params.plugin.origin === "bundled" || activationState.explicitlyEnabled) + ); +} + function canStartRequiredAgentHarnessPlugin(params: { plugin: InstalledPluginIndexRecord; pluginsConfig: ReturnType; @@ -785,13 +933,13 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: { const requiredAgentHarnessRuntimes = new Set( collectConfiguredAgentHarnessRuntimes(activationSourceConfig, params.env, { includeEnvRuntime: false, - includeLegacyAgentRuntimes: false, }), ); const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config); const configuredSpeechProviderIds = collectConfiguredSpeechProviderIds(activationSourceConfig); const configuredWebSearchProviderIds = collectConfiguredWebSearchProviderIds(activationSourceConfig); + const configuredModelProviderIds = collectConfiguredAgentModelProviderIds(activationSourceConfig); const configuredGenerationProviderIds = collectConfiguredGenerationProviderIds(activationSourceConfig); const normalizePluginId = createPluginRegistryIdNormalizer(params.index, { @@ -875,6 +1023,19 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: { ) { return true; } + if ( + canStartConfiguredModelProviderPlugin({ + plugin, + manifest, + config: params.config, + pluginsConfig, + activationSource, + configuredModelProviderIds, + platform: params.platform, + }) + ) { + return true; + } if ( canStartConfiguredGenerationProviderPlugin({ plugin, diff --git a/src/plugins/hook-types.ts b/src/plugins/hook-types.ts index e302ac35a0a..60be7501061 100644 --- a/src/plugins/hook-types.ts +++ b/src/plugins/hook-types.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../agents/runtime/index.js"; import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js"; import type { ReplyPayload } from "../auto-reply/reply-payload.js"; import type { diff --git a/src/plugins/hooks.sync-only.test.ts b/src/plugins/hooks.sync-only.test.ts index b825f8f94d9..ed9c1e4a7e4 100644 --- a/src/plugins/hooks.sync-only.test.ts +++ b/src/plugins/hooks.sync-only.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { describe, expect, it, vi } from "vitest"; import { createHookRunner, type HookRunnerLogger } from "./hooks.js"; import { createMockPluginRegistry } from "./hooks.test-helpers.js"; diff --git a/src/plugins/host-hook-turn-types.ts b/src/plugins/host-hook-turn-types.ts index 0d250c3aaff..dc9f86fb20c 100644 --- a/src/plugins/host-hook-turn-types.ts +++ b/src/plugins/host-hook-turn-types.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../agents/runtime/index.js"; import type { PluginJsonValue } from "./host-hook-json.js"; export type PluginNextTurnInjectionPlacement = "prepend_context" | "append_context"; diff --git a/src/plugins/pi-package-graph.test.ts b/src/plugins/pi-package-graph.test.ts deleted file mode 100644 index 79a9d73ae18..00000000000 --- a/src/plugins/pi-package-graph.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import fs from "node:fs"; -import path from "node:path"; -import { describe, expect, it } from "vitest"; -import YAML from "yaml"; - -type RootPackageManifest = { - dependencies?: Record; -}; - -type PnpmWorkspaceConfig = { - overrides?: Record; -}; - -const PI_PACKAGE_NAMES = [ - "@earendil-works/pi-agent-core", - "@earendil-works/pi-ai", - "@earendil-works/pi-coding-agent", - "@earendil-works/pi-tui", -] as const; - -function readRootManifest(): RootPackageManifest { - const manifestPath = path.resolve(process.cwd(), "package.json"); - return JSON.parse(fs.readFileSync(manifestPath, "utf8")) as RootPackageManifest; -} - -function readPnpmWorkspaceConfig(): PnpmWorkspaceConfig { - const workspacePath = path.resolve(process.cwd(), "pnpm-workspace.yaml"); - return YAML.parse(fs.readFileSync(workspacePath, "utf8")) as PnpmWorkspaceConfig; -} - -function isExactPinnedVersion(spec: string): boolean { - return !spec.startsWith("^") && !spec.startsWith("~"); -} - -function isPiOverrideKey(key: string): boolean { - return key.startsWith("@mariozechner/pi-") || key.includes("@mariozechner/pi-"); -} - -function readPiDependencySpecs() { - const dependencies = readRootManifest().dependencies ?? {}; - return PI_PACKAGE_NAMES.map((name) => ({ - name, - spec: dependencies[name], - })); -} - -function collectMissingSpecNames(specs: Array<{ name: string; spec?: string }>): string[] { - const names: string[] = []; - for (const entry of specs) { - if (!entry.spec) { - names.push(entry.name); - } - } - return names; -} - -function expectNoGraphViolations(violations: string[], message: string) { - expect(violations, message).toStrictEqual([]); -} - -describe("pi package graph guardrails", () => { - it("keeps root Pi packages aligned to the same exact version", () => { - const specs = readPiDependencySpecs(); - - const missing = collectMissingSpecNames(specs); - expectNoGraphViolations( - missing, - `Missing required root Pi dependencies: ${missing.join(", ") || ""}. Mixed or incomplete Pi root dependencies create an unsupported package graph.`, - ); - - const presentSpecs = specs.map((entry) => entry.spec); - const uniqueSpecs = [...new Set(presentSpecs)]; - expect( - uniqueSpecs, - `Root Pi dependencies must stay aligned to one exact version. Found: ${specs.map((entry) => `${entry.name}=${entry.spec}`).join(", ")}. Mixed Pi versions create an unsupported package graph.`, - ).toHaveLength(1); - - const inexact = specs.filter((entry) => !isExactPinnedVersion(entry.spec)); - expectNoGraphViolations( - inexact.map((entry) => `${entry.name}=${entry.spec}`), - `Root Pi dependencies must use exact pins, not ranges. Found: ${inexact.map((entry) => `${entry.name}=${entry.spec}`).join(", ") || ""}. Range-based Pi specs can silently create an unsupported package graph.`, - ); - }); - - it("forbids pnpm overrides that target Pi packages", () => { - const pnpmWorkspace = readPnpmWorkspaceConfig(); - const overrides = pnpmWorkspace.overrides ?? {}; - const piOverrides = Object.keys(overrides).filter(isPiOverrideKey); - - expectNoGraphViolations( - piOverrides, - `pnpm-workspace.yaml overrides must not target Pi packages. Found: ${piOverrides.join(", ") || ""}. Pi-specific overrides can silently create an unsupported package graph.`, - ); - }); -}); diff --git a/src/plugins/provider-auth-helpers.ts b/src/plugins/provider-auth-helpers.ts index 7a3b27a57dd..b5a453b7612 100644 --- a/src/plugins/provider-auth-helpers.ts +++ b/src/plugins/provider-auth-helpers.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import type { OAuthCredentials } from "@earendil-works/pi-ai"; +import type { OAuthCredentials } from "openclaw/plugin-sdk/llm"; import { resolveDefaultAgentDir } from "../agents/agent-scope-config.js"; import { buildAuthProfileId } from "../agents/auth-profiles/identity.js"; import { upsertAuthProfile, upsertAuthProfileWithLock } from "../agents/auth-profiles/profiles.js"; diff --git a/src/plugins/provider-model-compat.ts b/src/plugins/provider-model-compat.ts index 9f8dc2ccd3e..745803f44c8 100644 --- a/src/plugins/provider-model-compat.ts +++ b/src/plugins/provider-model-compat.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { detectOpenAICompletionsCompat } from "../agents/openai-completions-compat.js"; import type { ModelCompatConfig } from "../config/types.models.js"; import { normalizeStringEntries } from "../shared/string-normalization.js"; @@ -74,11 +74,11 @@ export function shouldOmitEmptyArrayItems( return compat?.omitEmptyArrayItems === true; } -function isOpenAiCompletionsModel(model: Model): model is Model<"openai-completions"> { +function isOpenAiCompletionsModel(model: Model): model is Model<"openai-completions"> { return model.api === "openai-completions"; } -function isAnthropicMessagesModel(model: Model): model is Model<"anthropic-messages"> { +function isAnthropicMessagesModel(model: Model): model is Model<"anthropic-messages"> { return model.api === "anthropic-messages"; } @@ -86,7 +86,7 @@ function normalizeAnthropicBaseUrl(baseUrl: string): string { return baseUrl.replace(/\/v1\/?$/, ""); } -export function normalizeModelCompat(model: Model): Model { +export function normalizeModelCompat(model: Model): Model { const baseUrl = model.baseUrl ?? ""; if (isAnthropicMessagesModel(model) && baseUrl) { diff --git a/src/plugins/provider-model-helpers.test.ts b/src/plugins/provider-model-helpers.test.ts index dab89a4cd8b..7f20a7ff3c4 100644 --- a/src/plugins/provider-model-helpers.test.ts +++ b/src/plugins/provider-model-helpers.test.ts @@ -1,4 +1,4 @@ -import type { ModelRegistry } from "@earendil-works/pi-coding-agent"; +import type { ModelRegistry } from "openclaw/plugin-sdk/agent-sessions"; import { describe, expect, it } from "vitest"; import { cloneFirstTemplateModel, matchesExactOrPrefix } from "./provider-model-helpers.js"; import type { ProviderRuntimeModel } from "./provider-runtime-model.types.js"; diff --git a/src/plugins/provider-openai-codex-oauth.test.ts b/src/plugins/provider-openai-codex-oauth.test.ts index 5dccbe478c9..fff22155579 100644 --- a/src/plugins/provider-openai-codex-oauth.test.ts +++ b/src/plugins/provider-openai-codex-oauth.test.ts @@ -8,9 +8,9 @@ const mocks = vi.hoisted(() => ({ formatOpenAIOAuthTlsPreflightFix: vi.fn(), })); -vi.mock("@earendil-works/pi-ai/oauth", async () => { - const actual = await vi.importActual( - "@earendil-works/pi-ai/oauth", +vi.mock("openclaw/plugin-sdk/llm-oauth", async () => { + const actual = await vi.importActual( + "openclaw/plugin-sdk/llm-oauth", ); return { ...actual, @@ -136,7 +136,7 @@ describe("loginOpenAICodexOAuth", () => { expect(runtime.error).not.toHaveBeenCalled(); }); - it("passes through Pi-provided authorize URLs without mutation", async () => { + it("passes through runtime-provided authorize URLs without mutation", async () => { const creds = createCodexCredentials(); mocks.loginOpenAICodex.mockImplementation( async (opts: { onAuth: (event: { url: string }) => Promise }) => { diff --git a/src/plugins/provider-openai-codex-oauth.ts b/src/plugins/provider-openai-codex-oauth.ts index 6b6e2f4b4f3..d3db6994f10 100644 --- a/src/plugins/provider-openai-codex-oauth.ts +++ b/src/plugins/provider-openai-codex-oauth.ts @@ -1,4 +1,4 @@ -import { loginOpenAICodex, type OAuthCredentials } from "@earendil-works/pi-ai/oauth"; +import { loginOpenAICodex, type OAuthCredentials } from "openclaw/plugin-sdk/llm-oauth"; import { formatErrorMessage } from "../infra/errors.js"; import { ensureGlobalUndiciEnvProxyDispatcher } from "../infra/net/undici-global-dispatcher.js"; import type { RuntimeEnv } from "../runtime.js"; diff --git a/src/plugins/provider-replay-helpers.ts b/src/plugins/provider-replay-helpers.ts index 2180dbd4d24..46765d63e7a 100644 --- a/src/plugins/provider-replay-helpers.ts +++ b/src/plugins/provider-replay-helpers.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../agents/runtime/index.js"; import { isGemma4ModelId } from "../shared/google-models.js"; import { sanitizeGoogleAssistantFirstOrdering } from "../shared/google-turn-ordering.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; diff --git a/src/plugins/provider-runtime-model.types.ts b/src/plugins/provider-runtime-model.types.ts index d58107cf532..a4d2d621750 100644 --- a/src/plugins/provider-runtime-model.types.ts +++ b/src/plugins/provider-runtime-model.types.ts @@ -1,11 +1,11 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; import type { ModelCompatConfig, ModelMediaInputConfig } from "../config/types.models.js"; +import type { Model } from "openclaw/plugin-sdk/llm"; /** * Fully-resolved runtime model shape used after provider/plugin-owned * discovery, overrides, and compat normalization. */ -export type ProviderRuntimeModel = Omit, "compat"> & { +export type ProviderRuntimeModel = Omit & { compat?: ModelCompatConfig; contextTokens?: number; params?: Record; diff --git a/src/plugins/provider-runtime.test.ts b/src/plugins/provider-runtime.test.ts index d0f45881561..4d8277d1aea 100644 --- a/src/plugins/provider-runtime.test.ts +++ b/src/plugins/provider-runtime.test.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "openclaw/plugin-sdk/agent-core"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ModelProviderConfig, OpenClawConfig } from "../config/types.js"; import type { ProviderRuntimeModel } from "./provider-runtime-model.types.js"; diff --git a/src/plugins/runtime-plugins.runtime.ts b/src/plugins/runtime-plugins.runtime.ts new file mode 100644 index 00000000000..009782a365f --- /dev/null +++ b/src/plugins/runtime-plugins.runtime.ts @@ -0,0 +1 @@ +export { ensureRuntimePluginsLoaded } from "../agents/runtime-plugins.js"; diff --git a/src/plugins/runtime/index.test.ts b/src/plugins/runtime/index.test.ts index cc6ac9b0f30..af4c6339f7b 100644 --- a/src/plugins/runtime/index.test.ts +++ b/src/plugins/runtime/index.test.ts @@ -310,6 +310,7 @@ describe("plugin runtime command execution", () => { "resolveThinkingPolicy", "resolveAgentDir", ]); + expect(runtime.agent.runEmbeddedPiAgent).toBe(runtime.agent.runEmbeddedAgent); expectFunctionKeys(runtime.agent.session as Record, [ "getSessionEntry", "listSessionEntries", diff --git a/src/plugins/runtime/runtime-agent.ts b/src/plugins/runtime/runtime-agent.ts index 613c9c88f1a..0aea8eb355b 100644 --- a/src/plugins/runtime/runtime-agent.ts +++ b/src/plugins/runtime/runtime-agent.ts @@ -24,8 +24,8 @@ import { createLazyRuntimeMethod, createLazyRuntimeModule } from "../../shared/l import { defineCachedValue } from "./runtime-cache.js"; import type { PluginRuntime } from "./types.js"; -const loadEmbeddedPiRuntime = createLazyRuntimeModule( - () => import("./runtime-embedded-pi.runtime.js"), +const loadEmbeddedAgentRuntime = createLazyRuntimeModule( + () => import("./runtime-embedded-agent.runtime.js"), ); function resolveRuntimeThinkingCatalog( @@ -68,10 +68,12 @@ export function createRuntimeAgent(): PluginRuntime["agent"] { Partial>; defineCachedValue(agentRuntime, "runEmbeddedAgent", () => - createLazyRuntimeMethod(loadEmbeddedPiRuntime, (runtime) => runtime.runEmbeddedAgent), + createLazyRuntimeMethod(loadEmbeddedAgentRuntime, (runtime) => runtime.runEmbeddedAgent), ); - defineCachedValue(agentRuntime, "runEmbeddedPiAgent", () => - createLazyRuntimeMethod(loadEmbeddedPiRuntime, (runtime) => runtime.runEmbeddedPiAgent), + defineCachedValue( + agentRuntime, + "runEmbeddedPiAgent", + () => (agentRuntime as PluginRuntime["agent"]).runEmbeddedAgent, ); defineCachedValue(agentRuntime, "session", () => ({ resolveStorePath, diff --git a/src/plugins/runtime/runtime-embedded-agent.runtime.ts b/src/plugins/runtime/runtime-embedded-agent.runtime.ts new file mode 100644 index 00000000000..3a1b6f0e688 --- /dev/null +++ b/src/plugins/runtime/runtime-embedded-agent.runtime.ts @@ -0,0 +1 @@ +export { runEmbeddedAgent } from "../../agents/embedded-agent.js"; diff --git a/src/plugins/runtime/runtime-embedded-pi.runtime.ts b/src/plugins/runtime/runtime-embedded-pi.runtime.ts deleted file mode 100644 index 140d27fa397..00000000000 --- a/src/plugins/runtime/runtime-embedded-pi.runtime.ts +++ /dev/null @@ -1 +0,0 @@ -export { runEmbeddedAgent, runEmbeddedPiAgent } from "../../agents/pi-embedded.js"; diff --git a/src/plugins/runtime/runtime-llm.runtime.test.ts b/src/plugins/runtime/runtime-llm.runtime.test.ts index 8dce8dddb76..fc3e7f6304a 100644 --- a/src/plugins/runtime/runtime-llm.runtime.test.ts +++ b/src/plugins/runtime/runtime-llm.runtime.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { resolveContextEngineCapabilities } from "../../agents/pi-embedded-runner/context-engine-capabilities.js"; +import { resolveContextEngineCapabilities } from "../../agents/embedded-agent-runner/context-engine-capabilities.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { withPluginRuntimePluginIdScope } from "./gateway-request-scope.js"; import { createRuntimeLlm } from "./runtime-llm.runtime.js"; diff --git a/src/plugins/runtime/runtime-llm.runtime.ts b/src/plugins/runtime/runtime-llm.runtime.ts index 788f569a8a7..3a93a686d8c 100644 --- a/src/plugins/runtime/runtime-llm.runtime.ts +++ b/src/plugins/runtime/runtime-llm.runtime.ts @@ -1,4 +1,4 @@ -import type { Api, Message } from "@earendil-works/pi-ai"; +import type { Api, Message } from "openclaw/plugin-sdk/llm"; import { modelKey } from "../../agents/model-ref-shared.js"; import { normalizeModelRef } from "../../agents/model-selection.js"; import type { NormalizedUsage, UsageLike } from "../../agents/usage.js"; diff --git a/src/plugins/runtime/runtime-model-auth.runtime.ts b/src/plugins/runtime/runtime-model-auth.runtime.ts index f5f774fcf21..9e61f4ece27 100644 --- a/src/plugins/runtime/runtime-model-auth.runtime.ts +++ b/src/plugins/runtime/runtime-model-auth.runtime.ts @@ -1,4 +1,4 @@ -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Model } from "openclaw/plugin-sdk/llm"; import { getApiKeyForModel as resolveModelApiKey, resolveApiKeyForProvider as resolveProviderApiKey, @@ -24,7 +24,7 @@ export async function resolveApiKeyForProvider( * `prepareRuntimeAuth` exchange on top of the standard credential lookup. */ export async function getRuntimeAuthForModel(params: { - model: Model; + model: Model; cfg?: OpenClawConfig; workspaceDir?: string; }): Promise { diff --git a/src/plugins/runtime/runtime-web-channel-plugin.ts b/src/plugins/runtime/runtime-web-channel-plugin.ts index f4fd644d881..dbb4f4d75c7 100644 --- a/src/plugins/runtime/runtime-web-channel-plugin.ts +++ b/src/plugins/runtime/runtime-web-channel-plugin.ts @@ -1,4 +1,4 @@ -import type { AgentToolResult } from "@earendil-works/pi-agent-core"; +import type { AgentToolResult } from "../../agents/runtime/index.js"; import type { ChannelAgentTool } from "../../channels/plugins/types.core.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { diff --git a/src/plugins/runtime/types-core.ts b/src/plugins/runtime/types-core.ts index 65addaed42c..0277d6942f6 100644 --- a/src/plugins/runtime/types-core.ts +++ b/src/plugins/runtime/types-core.ts @@ -135,9 +135,9 @@ export type LlmCompleteResult = { }; }; -type RuntimeRunEmbeddedPiAgent = ( - params: import("../../agents/pi-embedded-runner/run/params.js").RunEmbeddedPiAgentParams, -) => Promise; +type RuntimeRunEmbeddedAgent = ( + params: import("../../agents/embedded-agent-runner/run/params.js").RunEmbeddedAgentParams, +) => Promise; /** Core runtime helpers exposed to trusted native plugins. */ export type PluginRuntimeCore = { @@ -197,8 +197,9 @@ export type PluginRuntimeCore = { resolveThinkingPolicy: ( params: PluginRuntimeThinkingPolicyRequest, ) => PluginRuntimeThinkingPolicy; - runEmbeddedAgent: RuntimeRunEmbeddedPiAgent; - runEmbeddedPiAgent: RuntimeRunEmbeddedPiAgent; + runEmbeddedAgent: RuntimeRunEmbeddedAgent; + /** @deprecated Use runEmbeddedAgent. */ + runEmbeddedPiAgent: RuntimeRunEmbeddedAgent; resolveAgentTimeoutMs: typeof import("../../agents/timeout.js").resolveAgentTimeoutMs; ensureAgentWorkspace: typeof import("../../agents/workspace.js").ensureAgentWorkspace; session: { @@ -326,13 +327,13 @@ export type PluginRuntimeCore = { modelAuth: { /** Resolve auth for a model. Only provider/model, optional cfg, and workspaceDir are used. */ getApiKeyForModel: (params: { - model: import("@earendil-works/pi-ai").Model; + model: import("openclaw/plugin-sdk/llm").Model; cfg?: import("../../config/types.openclaw.js").OpenClawConfig; workspaceDir?: string; }) => Promise; /** Resolve request-ready auth for a model, including provider runtime exchanges. */ getRuntimeAuthForModel: (params: { - model: import("@earendil-works/pi-ai").Model; + model: import("openclaw/plugin-sdk/llm").Model; cfg?: import("../../config/types.openclaw.js").OpenClawConfig; workspaceDir?: string; }) => Promise; diff --git a/src/plugins/types.ts b/src/plugins/types.ts index e95589a4b66..0b60d7df27a 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -1,8 +1,5 @@ import type { IncomingMessage, ServerResponse } from "node:http"; import type { Duplex } from "node:stream"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { StreamFn } from "@earendil-works/pi-agent-core"; -import type { ModelRegistry } from "@earendil-works/pi-coding-agent"; import type { Command } from "commander"; import type { ApiKeyCredential, @@ -10,9 +7,12 @@ import type { OAuthCredential, AuthProfileStore, } from "../agents/auth-profiles/types.js"; +import type { FailoverReason } from "../agents/embedded-agent-helpers/types.js"; import type { AgentHarness } from "../agents/harness/types.js"; import type { ModelCatalogEntry } from "../agents/model-catalog.types.js"; -import type { FailoverReason } from "../agents/pi-embedded-helpers/types.js"; +import type { AgentMessage } from "../agents/runtime/index.js"; +import type { StreamFn } from "../agents/runtime/index.js"; +import type { ModelRegistry } from "../agents/sessions/index.js"; import type { ProviderSystemPromptContribution } from "../agents/system-prompt-contribution.js"; import type { PromptMode } from "../agents/system-prompt.types.js"; import type { AnyAgentTool } from "../agents/tools/common.js"; @@ -868,7 +868,7 @@ export type ProviderReasoningOutputModeContext = ProviderReplayPolicyContext; /** * Provider-owned transport creation. * - * Use this when the provider needs to replace pi-ai's default transport with a + * Use this when the provider needs to replace shared model runtime's default transport with a * custom StreamFn (for example a native API transport that cannot be expressed * as a wrapper around `streamSimple`). */ @@ -886,7 +886,7 @@ export type ProviderCreateStreamFnContext = { * transport-independent wrappers. * * Use this for provider-specific payload/header/model mutations that still run - * through the normal `pi-ai` stream path. + * through the normal `shared model runtime` stream path. */ export type ProviderWrapStreamFnContext = ProviderPrepareExtraParamsContext & { model?: ProviderRuntimeModel; @@ -1740,7 +1740,7 @@ export type ProviderPlugin = { /** * Provider-owned OAuth refresh. * - * OpenClaw calls this before falling back to the shared `pi-ai` OAuth + * OpenClaw calls this before falling back to the shared `shared model runtime` OAuth * refreshers. Use it when the provider has a custom refresh endpoint, or when * the provider needs custom refresh-failure behavior that should stay out of * core auth-profile code. @@ -2014,6 +2014,8 @@ export type PluginCommandHandler = ( * Definition for a plugin-registered command. */ export const AGENT_PROMPT_SURFACE_KINDS = [ + "openclaw_main", + /** @deprecated Use openclaw_main. */ "pi_main", "codex_app_server", "cli_backend", diff --git a/src/plugins/wired-hooks-after-tool-call.e2e.test.ts b/src/plugins/wired-hooks-after-tool-call.e2e.test.ts index a45b8e89863..cb828720e44 100644 --- a/src/plugins/wired-hooks-after-tool-call.e2e.test.ts +++ b/src/plugins/wired-hooks-after-tool-call.e2e.test.ts @@ -1,8 +1,8 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; /** - * Test: after_tool_call hook wiring (pi-embedded-subscribe.handlers.tools.ts) + * Test: after_tool_call hook wiring (embedded-agent-subscribe.handlers.tools.ts) */ -import { createBaseToolHandlerState } from "../agents/pi-tool-handler-state.test-helpers.js"; +import { createBaseToolHandlerState } from "../agents/agent-tool-handler-state.test-helpers.js"; const hookMocks = vi.hoisted(() => ({ runner: { @@ -99,13 +99,13 @@ function expectAfterToolCallPayload(params: { expect(context).toEqual(params.expectedContext); } -let handleToolExecutionStart: typeof import("../agents/pi-embedded-subscribe.handlers.tools.js").handleToolExecutionStart; -let handleToolExecutionEnd: typeof import("../agents/pi-embedded-subscribe.handlers.tools.js").handleToolExecutionEnd; +let handleToolExecutionStart: typeof import("../agents/embedded-agent-subscribe.handlers.tools.js").handleToolExecutionStart; +let handleToolExecutionEnd: typeof import("../agents/embedded-agent-subscribe.handlers.tools.js").handleToolExecutionEnd; describe("after_tool_call hook wiring", () => { beforeAll(async () => { ({ handleToolExecutionStart, handleToolExecutionEnd } = - await import("../agents/pi-embedded-subscribe.handlers.tools.js")); + await import("../agents/embedded-agent-subscribe.handlers.tools.js")); }); beforeEach(() => { diff --git a/src/plugins/wired-hooks-compaction.test.ts b/src/plugins/wired-hooks-compaction.test.ts index c84177794d5..0524bf6f513 100644 --- a/src/plugins/wired-hooks-compaction.test.ts +++ b/src/plugins/wired-hooks-compaction.test.ts @@ -24,7 +24,7 @@ vi.mock("../infra/agent-events.js", () => ({ import { handleCompactionEnd, handleCompactionStart, -} from "../agents/pi-embedded-subscribe.handlers.compaction.js"; +} from "../agents/embedded-agent-subscribe.handlers.compaction.js"; describe("compaction hook wiring", () => { beforeEach(() => { diff --git a/src/process/exec.ts b/src/process/exec.ts index 514b8a7a618..cd91e178a32 100644 --- a/src/process/exec.ts +++ b/src/process/exec.ts @@ -351,7 +351,7 @@ export async function runCommandWithTimeout( ? { shell: true } : {}), }); - // Spawn with inherited stdin (TTY) so tools like `pi` stay interactive when needed. + // Spawn with inherited stdin (TTY) so interactive tools stay usable when needed. return await new Promise((resolve, reject) => { const stdoutChunks: Buffer[] = []; const stderrChunks: Buffer[] = []; diff --git a/src/scripts/control-ui-i18n.test.ts b/src/scripts/control-ui-i18n.test.ts index e106a208286..162ced5c017 100644 --- a/src/scripts/control-ui-i18n.test.ts +++ b/src/scripts/control-ui-i18n.test.ts @@ -1,12 +1,8 @@ -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; import { describe, expect, it } from "vitest"; import { - DEFAULT_PI_PACKAGE_VERSION, findPlaceholderMismatches, isProviderAuthError, - resolveLocalPiCommand, + resolveTranslationModel, } from "../../scripts/control-ui-i18n.ts"; describe("control-ui-i18n placeholder validation", () => { @@ -42,38 +38,12 @@ describe("control-ui-i18n placeholder validation", () => { }); }); -describe("control-ui-i18n pi runtime resolution", () => { - it("keeps the fallback pi package version aligned with the workspace dependency", async () => { - const packageJson = JSON.parse(await readFile("package.json", "utf8")) as { - dependencies?: Record; - }; - - expect(DEFAULT_PI_PACKAGE_VERSION).toBe( - packageJson.dependencies?.["@earendil-works/pi-coding-agent"], - ); - }); - - it("uses the workspace pi runtime before falling back to npm installation", async () => { - const root = await mkdtemp(path.join(tmpdir(), "openclaw-control-ui-i18n-")); - try { - const cliPath = path.join( - root, - "node_modules", - "@earendil-works", - "pi-coding-agent", - "dist", - "cli.js", - ); - await mkdir(path.dirname(cliPath), { recursive: true }); - await writeFile(cliPath, "#!/usr/bin/env node\n", "utf8"); - - expect(resolveLocalPiCommand(root)).toEqual({ - executable: "node", - args: [cliPath], - }); - } finally { - await rm(root, { force: true, recursive: true }); - } +describe("control-ui-i18n translation runtime resolution", () => { + it("uses the in-tree OpenClaw LLM model catalog", () => { + expect(resolveTranslationModel()).toMatchObject({ + id: "gpt-5.5", + provider: "openai", + }); }); }); diff --git a/src/secrets/runtime-fast-path.ts b/src/secrets/runtime-fast-path.ts index f1a37efd0a0..b67dd371206 100644 --- a/src/secrets/runtime-fast-path.ts +++ b/src/secrets/runtime-fast-path.ts @@ -32,7 +32,6 @@ const RUNTIME_PATH_ENV_KEYS = [ "OPENCLAW_STATE_DIR", "OPENCLAW_CONFIG_PATH", "OPENCLAW_AGENT_DIR", - "PI_CODING_AGENT_DIR", "OPENCLAW_TEST_FAST", ] as const; diff --git a/src/secrets/storage-scan.ts b/src/secrets/storage-scan.ts index 6d1d46bab9d..26f4845fa33 100644 --- a/src/secrets/storage-scan.ts +++ b/src/secrets/storage-scan.ts @@ -35,9 +35,9 @@ export function listLegacyAuthJsonPaths(stateDir: string): string[] { } function resolveActiveAgentDir(stateDir: string, env: NodeJS.ProcessEnv = process.env): string { - const override = env.OPENCLAW_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim(); + const override = env.OPENCLAW_AGENT_DIR?.trim(); if (override) { - return resolveUserPath(override); + return resolveUserPath(override, env); } return path.join(resolveUserPath(stateDir), "agents", "main", "agent"); } diff --git a/src/security/audit-extra.summary.ts b/src/security/audit-extra.summary.ts index 424791b7554..fe9e719b693 100644 --- a/src/security/audit-extra.summary.ts +++ b/src/security/audit-extra.summary.ts @@ -1,5 +1,5 @@ +import { resolveProviderToolPolicy } from "../agents/agent-tools.policy.js"; import { parseModelRef } from "../agents/model-selection-normalize.js"; -import { resolveProviderToolPolicy } from "../agents/pi-tools.policy.js"; import { resolveSandboxConfigForAgent } from "../agents/sandbox/config.js"; import { resolveSandboxToolPolicyForAgent } from "../agents/sandbox/tool-policy.js"; import type { SandboxToolPolicy } from "../agents/sandbox/types.js"; diff --git a/src/sessions/input-provenance.ts b/src/sessions/input-provenance.ts index 0b15c4efeaf..aa7e4320b6b 100644 --- a/src/sessions/input-provenance.ts +++ b/src/sessions/input-provenance.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../agents/runtime/index.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; export const INPUT_PROVENANCE_KIND_VALUES = [ diff --git a/src/shared/google-turn-ordering.ts b/src/shared/google-turn-ordering.ts index db6192f2bfa..3336901b3fc 100644 --- a/src/shared/google-turn-ordering.ts +++ b/src/shared/google-turn-ordering.ts @@ -1,4 +1,4 @@ -import type { AgentMessage } from "@earendil-works/pi-agent-core"; +import type { AgentMessage } from "../agents/runtime/index.js"; const GOOGLE_TURN_ORDER_BOOTSTRAP_TEXT = "(session bootstrap)"; diff --git a/src/shared/node-match.test.ts b/src/shared/node-match.test.ts index 03c093cca9e..5fa6103483c 100644 --- a/src/shared/node-match.test.ts +++ b/src/shared/node-match.test.ts @@ -19,7 +19,7 @@ describe("shared/node-match", () => { expect(resolveNodeMatches(nodes, "mac studio")).toEqual([nodes[0]]); expect(resolveNodeMatches(nodes, " Mac---Studio!! ")).toEqual([nodes[0]]); expect(resolveNodeMatches(nodes, "pi-456")).toEqual([nodes[1]]); - expect(resolveNodeMatches(nodes, "pi")).toStrictEqual([]); + expect(resolveNodeMatches(nodes, "openclaw")).toStrictEqual([]); expect(resolveNodeMatches(nodes, " ")).toStrictEqual([]); }); diff --git a/src/shared/session-types.ts b/src/shared/session-types.ts index b53dcea1afa..768ac60f51f 100644 --- a/src/shared/session-types.ts +++ b/src/shared/session-types.ts @@ -13,7 +13,7 @@ export type GatewayAgentModel = { export type GatewayAgentRuntime = { id: string; - fallback?: "pi" | "none"; + fallback?: "openclaw" | "none"; source: "env" | "agent" | "defaults" | "model" | "provider" | "implicit" | "session-key"; }; diff --git a/src/status/agent-runtime-label.ts b/src/status/agent-runtime-label.ts index e532d5cfb08..5f768db1dbe 100644 --- a/src/status/agent-runtime-label.ts +++ b/src/status/agent-runtime-label.ts @@ -8,7 +8,7 @@ import { import { sanitizeTerminalText } from "../terminal/safe-text.js"; const AGENT_RUNTIME_LABELS: Readonly> = { - pi: "OpenClaw Pi Default", + openclaw: "OpenClaw Default", codex: "OpenAI Codex", "codex-cli": "OpenAI Codex", "claude-cli": "Claude CLI", @@ -50,5 +50,5 @@ export function resolveAgentRuntimeLabel(args: { ); } - return AGENT_RUNTIME_LABELS.pi; + return AGENT_RUNTIME_LABELS.openclaw; } diff --git a/src/status/status-message.ts b/src/status/status-message.ts index 1ef4e0de85c..7e40d141ecb 100644 --- a/src/status/status-message.ts +++ b/src/status/status-message.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import { resolveContextTokensForModel } from "../agents/context.js"; import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; +import { resolveExtraParams } from "../agents/embedded-agent-runner/extra-params.js"; import { resolveModelAuthMode } from "../agents/model-auth.js"; import { areRuntimeModelRefsEquivalent, @@ -12,7 +13,6 @@ import { resolveModelRefFromString, } from "../agents/model-selection.js"; import { resolveOpenAITextVerbosity } from "../agents/openai-text-verbosity.js"; -import { resolveExtraParams } from "../agents/pi-embedded-runner/extra-params.js"; import { resolveSandboxRuntimeStatus } from "../agents/sandbox.js"; import { formatProviderModelRef, diff --git a/src/talk/agent-consult-runtime.test.ts b/src/talk/agent-consult-runtime.test.ts index 33473d82eb4..1b6bca4a63f 100644 --- a/src/talk/agent-consult-runtime.test.ts +++ b/src/talk/agent-consult-runtime.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import type { RunEmbeddedPiAgentParams } from "../agents/pi-embedded-runner/run/params.js"; +import type { RunEmbeddedAgentParams } from "../agents/embedded-agent-runner/run/params.js"; import { setRealtimeVoiceAgentConsultDepsForTest, consultRealtimeVoiceAgent, @@ -30,7 +30,7 @@ function createAgentRuntime(payloads: unknown[] = [{ text: "Speak this." }]) { lastThreadId?: string | number; } > = {}; - const runEmbeddedPiAgent = vi.fn(async () => ({ + const runEmbeddedAgent = vi.fn(async () => ({ payloads, meta: {}, })); @@ -90,25 +90,25 @@ function createAgentRuntime(payloads: unknown[] = [{ text: "Speak this." }]) { entry?.sessionFile ?? "/tmp/session.json", ), }, - runEmbeddedPiAgent, + runEmbeddedAgent, }, - runEmbeddedPiAgent, + runEmbeddedAgent, sessionStore, }; } -function requireEmbeddedPiAgentCall(runEmbeddedPiAgent: { +function requireEmbeddedAgentCall(runEmbeddedAgent: { mock: { calls: unknown[][] }; -}): RunEmbeddedPiAgentParams { - const [call] = runEmbeddedPiAgent.mock.calls; +}): RunEmbeddedAgentParams { + const [call] = runEmbeddedAgent.mock.calls; if (!call) { - throw new Error("Expected embedded PI agent call"); + throw new Error("Expected embedded OpenClaw agent call"); } const [params] = call; if (typeof params !== "object" || params === null || Array.isArray(params)) { - throw new Error("Expected embedded PI agent params to be an object"); + throw new Error("Expected embedded OpenClaw agent params to be an object"); } - return params as RunEmbeddedPiAgentParams; + return params as RunEmbeddedAgentParams; } function expectPositiveTimestamp(value: unknown) { @@ -144,7 +144,7 @@ describe("realtime voice agent consult runtime", () => { }); it("runs an embedded agent using the shared session and prompt contract", async () => { - const { runtime, runEmbeddedPiAgent, sessionStore } = createAgentRuntime(); + const { runtime, runEmbeddedAgent, sessionStore } = createAgentRuntime(); const result = await consultRealtimeVoiceAgent({ cfg: {} as never, @@ -175,7 +175,7 @@ describe("realtime voice agent consult runtime", () => { expect(Object.keys(voiceSession).toSorted()).toStrictEqual(["sessionId", "updatedAt"]); expectNonEmptyString(voiceSession.sessionId); expectPositiveTimestamp(voiceSession.updatedAt); - const call = requireEmbeddedPiAgentCall(runEmbeddedPiAgent); + const call = requireEmbeddedAgentCall(runEmbeddedAgent); expect(call.sessionId).toBe(voiceSession.sessionId); expect(call.sessionKey).toBe("voice:15550001234"); expect(call.sandboxSessionKey).toBe("agent:main:voice:15550001234"); @@ -205,7 +205,7 @@ describe("realtime voice agent consult runtime", () => { }); it("scopes sandbox resolution to the configured consult agent", async () => { - const { runtime, runEmbeddedPiAgent } = createAgentRuntime(); + const { runtime, runEmbeddedAgent } = createAgentRuntime(); await consultRealtimeVoiceAgent({ cfg: {} as never, @@ -222,7 +222,7 @@ describe("realtime voice agent consult runtime", () => { userLabel: "Caller", }); - const call = requireEmbeddedPiAgentCall(runEmbeddedPiAgent); + const call = requireEmbeddedAgentCall(runEmbeddedAgent); expect(call.sessionKey).toBe("voice:15550001234"); expect(call.sandboxSessionKey).toBe("agent:voice:voice:15550001234"); expect(call.agentId).toBe("voice"); @@ -254,7 +254,7 @@ describe("realtime voice agent consult runtime", () => { }); it("forks requester context when fork mode has a parent session", async () => { - const { runtime, runEmbeddedPiAgent, sessionStore } = createAgentRuntime(); + const { runtime, runEmbeddedAgent, sessionStore } = createAgentRuntime(); sessionStore["agent:main:main"] = { sessionId: "parent-session", sessionFile: "/tmp/parent.jsonl", @@ -313,14 +313,14 @@ describe("realtime voice agent consult runtime", () => { updatedAt: forkedEntry.updatedAt, }); expectPositiveTimestamp(forkedEntry.updatedAt); - const call = requireEmbeddedPiAgentCall(runEmbeddedPiAgent); + const call = requireEmbeddedAgentCall(runEmbeddedAgent); expect(call.sessionId).toBe("forked-session"); expect(call.sessionFile).toBe("/tmp/forked.jsonl"); expect(call.spawnedBy).toBe("agent:main:main"); }); it("inherits requester message routing for forked consult sessions", async () => { - const { runtime, runEmbeddedPiAgent, sessionStore } = createAgentRuntime(); + const { runtime, runEmbeddedAgent, sessionStore } = createAgentRuntime(); sessionStore["agent:main:discord:channel:123"] = { sessionId: "parent-session", deliveryContext: { @@ -348,7 +348,7 @@ describe("realtime voice agent consult runtime", () => { userLabel: "Caller", }); - const call = requireEmbeddedPiAgentCall(runEmbeddedPiAgent); + const call = requireEmbeddedAgentCall(runEmbeddedAgent); expect(call.sessionKey).toBe("voice:google-meet:meet-1"); expect(call.spawnedBy).toBe("agent:main:discord:channel:123"); expect(call.messageProvider).toBe("discord"); @@ -378,7 +378,7 @@ describe("realtime voice agent consult runtime", () => { }); it("reuses the call session delivery context when requester metadata is absent", async () => { - const { runtime, runEmbeddedPiAgent, sessionStore } = createAgentRuntime(); + const { runtime, runEmbeddedAgent, sessionStore } = createAgentRuntime(); sessionStore["voice:google-meet:meet-1"] = { sessionId: "call-session", deliveryContext: { @@ -405,7 +405,7 @@ describe("realtime voice agent consult runtime", () => { userLabel: "Caller", }); - const call = requireEmbeddedPiAgentCall(runEmbeddedPiAgent); + const call = requireEmbeddedAgentCall(runEmbeddedAgent); expect(call.sessionId).toBe("call-session"); expect(call.sessionKey).toBe("voice:google-meet:meet-1"); expect(call.messageProvider).toBe("discord"); diff --git a/src/talk/agent-consult-runtime.ts b/src/talk/agent-consult-runtime.ts index 24aafb1ba13..83d2d810cdb 100644 --- a/src/talk/agent-consult-runtime.ts +++ b/src/talk/agent-consult-runtime.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; import path from "node:path"; -import type { RunEmbeddedPiAgentParams } from "../agents/pi-embedded-runner/run/params.js"; +import type { RunEmbeddedAgentParams } from "../agents/embedded-agent-runner/run/params.js"; import { forkSessionFromParent, resolveParentForkDecision, @@ -207,10 +207,10 @@ export async function consultRealtimeVoiceAgent(params: { agentId?: string; spawnedBy?: string | null; contextMode?: RealtimeVoiceAgentConsultContextMode; - provider?: RunEmbeddedPiAgentParams["provider"]; - model?: RunEmbeddedPiAgentParams["model"]; - thinkLevel?: RunEmbeddedPiAgentParams["thinkLevel"]; - fastMode?: RunEmbeddedPiAgentParams["fastMode"]; + provider?: RunEmbeddedAgentParams["provider"]; + model?: RunEmbeddedAgentParams["model"]; + thinkLevel?: RunEmbeddedAgentParams["thinkLevel"]; + fastMode?: RunEmbeddedAgentParams["fastMode"]; timeoutMs?: number; toolsAllow?: string[]; extraSystemPrompt?: string; @@ -247,7 +247,7 @@ export async function consultRealtimeVoiceAgent(params: { const sessionFile = params.agentRuntime.session.resolveSessionFilePath(sessionId, sessionEntry, { agentId, }); - const result = await params.agentRuntime.runEmbeddedPiAgent({ + const result = await params.agentRuntime.runEmbeddedAgent({ sessionId, sessionKey: params.sessionKey, sandboxSessionKey: resolveRealtimeVoiceAgentSandboxSessionKey(agentId, params.sessionKey), diff --git a/src/tasks/task-status.ts b/src/tasks/task-status.ts index 1f37d9815be..54eaa421b70 100644 --- a/src/tasks/task-status.ts +++ b/src/tasks/task-status.ts @@ -1,8 +1,8 @@ +import { sanitizeUserFacingText } from "../agents/embedded-agent-helpers/sanitize-user-facing-text.js"; import { INTERNAL_RUNTIME_CONTEXT_BEGIN, INTERNAL_RUNTIME_CONTEXT_END, } from "../agents/internal-runtime-context.js"; -import { sanitizeUserFacingText } from "../agents/pi-embedded-helpers/sanitize-user-facing-text.js"; import { truncateUtf16Safe } from "../utils.js"; import type { TaskRecord } from "./task-registry.types.js"; diff --git a/src/test-utils/openclaw-test-state.test.ts b/src/test-utils/openclaw-test-state.test.ts index d46c8715bf8..01772548a74 100644 --- a/src/test-utils/openclaw-test-state.test.ts +++ b/src/test-utils/openclaw-test-state.test.ts @@ -68,9 +68,7 @@ describe("openclaw test state", () => { it("clears inherited agent-dir overrides by default", async () => { const previousAgentDir = process.env.OPENCLAW_AGENT_DIR; - const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR; process.env.OPENCLAW_AGENT_DIR = "/tmp/outside-openclaw-agent"; - process.env.PI_CODING_AGENT_DIR = "/tmp/outside-pi-agent"; try { const state = await createOpenClawTestState({ @@ -79,27 +77,19 @@ describe("openclaw test state", () => { try { expect(process.env.OPENCLAW_AGENT_DIR).toBeUndefined(); - expect(process.env.PI_CODING_AGENT_DIR).toBeUndefined(); expect(state.env.OPENCLAW_AGENT_DIR).toBeUndefined(); - expect(state.env.PI_CODING_AGENT_DIR).toBeUndefined(); expect(state.agentDir()).toBe(path.join(state.stateDir, "agents", "main", "agent")); } finally { await state.cleanup(); } expect(process.env.OPENCLAW_AGENT_DIR).toBe("/tmp/outside-openclaw-agent"); - expect(process.env.PI_CODING_AGENT_DIR).toBe("/tmp/outside-pi-agent"); } finally { if (previousAgentDir === undefined) { delete process.env.OPENCLAW_AGENT_DIR; } else { process.env.OPENCLAW_AGENT_DIR = previousAgentDir; } - if (previousPiAgentDir === undefined) { - delete process.env.PI_CODING_AGENT_DIR; - } else { - process.env.PI_CODING_AGENT_DIR = previousPiAgentDir; - } } }); @@ -108,14 +98,11 @@ describe("openclaw test state", () => { { env: { OPENCLAW_AGENT_DIR: "/tmp/explicit-openclaw-agent", - PI_CODING_AGENT_DIR: "/tmp/explicit-pi-agent", }, }, async (state) => { expect(process.env.OPENCLAW_AGENT_DIR).toBe("/tmp/explicit-openclaw-agent"); - expect(process.env.PI_CODING_AGENT_DIR).toBe("/tmp/explicit-pi-agent"); expect(state.env.OPENCLAW_AGENT_DIR).toBe("/tmp/explicit-openclaw-agent"); - expect(state.env.PI_CODING_AGENT_DIR).toBe("/tmp/explicit-pi-agent"); }, ); }); @@ -127,9 +114,7 @@ describe("openclaw test state", () => { }, async (state) => { expect(process.env.OPENCLAW_AGENT_DIR).toBe(state.agentDir()); - expect(process.env.PI_CODING_AGENT_DIR).toBe(state.agentDir()); expect(state.env.OPENCLAW_AGENT_DIR).toBe(state.agentDir()); - expect(state.env.PI_CODING_AGENT_DIR).toBe(state.agentDir()); }, ); }); diff --git a/src/test-utils/openclaw-test-state.ts b/src/test-utils/openclaw-test-state.ts index 7950c5e8bf9..9d8c677c964 100644 --- a/src/test-utils/openclaw-test-state.ts +++ b/src/test-utils/openclaw-test-state.ts @@ -60,7 +60,6 @@ const ENV_KEYS = [ "OPENCLAW_STATE_DIR", "OPENCLAW_CONFIG_PATH", "OPENCLAW_AGENT_DIR", - "PI_CODING_AGENT_DIR", "OPENCLAW_SERVICE_REPAIR_POLICY", ] as const; @@ -202,11 +201,9 @@ function buildEnvVars(params: { params.agentEnv === "main" ? { OPENCLAW_AGENT_DIR: params.agentDir, - PI_CODING_AGENT_DIR: params.agentDir, } : { OPENCLAW_AGENT_DIR: undefined, - PI_CODING_AGENT_DIR: undefined, }; const envVars: Record = { OPENCLAW_STATE_DIR: params.stateDir, diff --git a/src/trajectory/export.test.ts b/src/trajectory/export.test.ts index 53d6ff038e1..540c523d586 100644 --- a/src/trajectory/export.test.ts +++ b/src/trajectory/export.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import type { Message, Usage } from "@earendil-works/pi-ai"; +import type { Message, Usage } from "openclaw/plugin-sdk/llm"; import { afterAll, describe, expect, it } from "vitest"; import { exportTrajectoryBundle, resolveDefaultTrajectoryExportDir } from "./export.js"; import { TRAJECTORY_RUNTIME_FILE_MAX_BYTES, resolveTrajectoryPointerFilePath } from "./paths.js"; diff --git a/src/trajectory/export.ts b/src/trajectory/export.ts index 53ad0988532..7aead5e59b3 100644 --- a/src/trajectory/export.ts +++ b/src/trajectory/export.ts @@ -1,8 +1,8 @@ import fsp from "node:fs/promises"; import path from "node:path"; -import type { AgentMessage } from "@earendil-works/pi-agent-core"; -import type { FileEntry, SessionEntry, SessionHeader } from "@earendil-works/pi-coding-agent"; import { sanitizeDiagnosticPayload } from "../agents/payload-redaction.js"; +import type { AgentMessage } from "../agents/runtime/index.js"; +import type { FileEntry, SessionEntry, SessionHeader } from "../agents/sessions/index.js"; import { resolveStateDir } from "../config/paths.js"; import { jsonSupportBundleFile, diff --git a/src/trajectory/metadata.test.ts b/src/trajectory/metadata.test.ts index 79449b7ca62..b1fb31546d8 100644 --- a/src/trajectory/metadata.test.ts +++ b/src/trajectory/metadata.test.ts @@ -117,7 +117,7 @@ describe("trajectory metadata", () => { webSearchProviderIds: [], migrationProviderIds: [], memoryEmbeddingProviderIds: [], - agentHarnessIds: ["pi"], + agentHarnessIds: ["openclaw"], cliCommands: [], services: [], gatewayDiscoveryServiceIds: [], diff --git a/src/tts/tts-core.ts b/src/tts/tts-core.ts index 5a107c99cab..9ca1a50546b 100644 --- a/src/tts/tts-core.ts +++ b/src/tts/tts-core.ts @@ -1,4 +1,5 @@ -import { completeSimple, type TextContent } from "@earendil-works/pi-ai"; +import { completeSimple, type TextContent } from "openclaw/plugin-sdk/llm"; +import { resolveModelAsync } from "../agents/embedded-agent-runner/model.js"; import { getApiKeyForModel, requireApiKey } from "../agents/model-auth.js"; import { buildModelAliasIndex, @@ -6,7 +7,6 @@ import { resolveModelRefFromString, type ModelRef, } from "../agents/model-selection.js"; -import { resolveModelAsync } from "../agents/pi-embedded-runner/model.js"; import { prepareModelForSimpleCompletion } from "../agents/simple-completion-transport.js"; import type { OpenClawConfig } from "../config/types.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; diff --git a/src/tui/tui-event-handlers.ts b/src/tui/tui-event-handlers.ts index 3bdf7a5edb5..1a9f30956aa 100644 --- a/src/tui/tui-event-handlers.ts +++ b/src/tui/tui-event-handlers.ts @@ -1,4 +1,4 @@ -import { classifyFailoverReason, isAuthErrorMessage } from "../agents/pi-embedded-helpers.js"; +import { isAuthErrorMessage } from "../agents/embedded-agent-helpers.js"; import { parseAgentSessionKey } from "../sessions/session-key-utils.js"; import { formatRawAssistantErrorForUi } from "../shared/assistant-error-format.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; diff --git a/src/tui/tui-pty-harness.e2e.test.ts b/src/tui/tui-pty-harness.e2e.test.ts index 41c89887258..f73989f717e 100644 --- a/src/tui/tui-pty-harness.e2e.test.ts +++ b/src/tui/tui-pty-harness.e2e.test.ts @@ -220,7 +220,7 @@ async function writeTuiPtyFixtureScript(dir: string) { const scriptPath = path.join(dir, "run-tui-pty-fixture.ts"); const tuiModuleUrl = pathToFileURL(path.join(process.cwd(), "src/tui/tui.ts")).href; const payloadsModuleUrl = pathToFileURL( - path.join(process.cwd(), "src/agents/pi-embedded-runner/run/payloads.ts"), + path.join(process.cwd(), "src/agents/embedded-agent-runner/run/payloads.ts"), ).href; const replyPayloadModuleUrl = pathToFileURL( path.join(process.cwd(), "src/auto-reply/reply-payload.ts"), diff --git a/src/types/pi-agent-core.d.ts b/src/types/agent-core.d.ts similarity index 79% rename from src/types/pi-agent-core.d.ts rename to src/types/agent-core.d.ts index 5f0f90c6ee1..55320245fa0 100644 --- a/src/types/pi-agent-core.d.ts +++ b/src/types/agent-core.d.ts @@ -1,6 +1,6 @@ -import "@earendil-works/pi-agent-core"; +import "openclaw/plugin-sdk/agent-core"; -declare module "@earendil-works/pi-agent-core" { +declare module "openclaw/plugin-sdk/agent-core" { // OpenClaw persists compaction markers alongside normal agent history. interface CustomAgentMessages { compactionSummary: { diff --git a/src/types/agent-sessions.d.ts b/src/types/agent-sessions.d.ts new file mode 100644 index 00000000000..6b5b78ed34e --- /dev/null +++ b/src/types/agent-sessions.d.ts @@ -0,0 +1,8 @@ +export type OpenClawAgentSessionSkillSourceAugmentation = never; + +declare module "openclaw/plugin-sdk/agent-sessions" { + interface Skill { + // OpenClaw relies on the source identifier returned by skill loaders. + source: string; + } +} diff --git a/src/types/highlight-js-lib-index.d.ts b/src/types/highlight-js-lib-index.d.ts new file mode 100644 index 00000000000..40b5b19ccaa --- /dev/null +++ b/src/types/highlight-js-lib-index.d.ts @@ -0,0 +1,19 @@ +declare module "highlight.js/lib/index.js" { + interface HighlightResult { + value: string; + } + + interface HighlightOptions { + language: string; + ignoreIllegals?: boolean; + } + + interface HighlightJs { + highlight(code: string, options: HighlightOptions): HighlightResult; + highlightAuto(code: string, languageSubset?: string[]): HighlightResult; + getLanguage(name: string): unknown; + } + + const hljs: HighlightJs; + export default hljs; +} diff --git a/src/types/pi-coding-agent.d.ts b/src/types/pi-coding-agent.d.ts deleted file mode 100644 index 008ae2f91f1..00000000000 --- a/src/types/pi-coding-agent.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type OpenClawPiCodingAgentSkillSourceAugmentation = never; - -declare module "@earendil-works/pi-coding-agent" { - interface Skill { - // OpenClaw relies on the source identifier returned by pi skill loaders. - source: string; - } -} diff --git a/test/helpers/agents/happy-path-prompt-snapshots.ts b/test/helpers/agents/happy-path-prompt-snapshots.ts index 9e623840ee4..1acac610bac 100644 --- a/test/helpers/agents/happy-path-prompt-snapshots.ts +++ b/test/helpers/agents/happy-path-prompt-snapshots.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import path from "node:path"; -import type { Api, Model } from "@earendil-works/pi-ai"; +import type { Api, Model } from "openclaw/plugin-sdk/llm"; import { resolveHeartbeatPromptForResponseTool } from "../../../src/auto-reply/heartbeat.js"; import { buildDirectChatContext, diff --git a/test/helpers/agents/pi-ai-stream-simple-mock.ts b/test/helpers/agents/llm-stream-simple-mock.ts similarity index 73% rename from test/helpers/agents/pi-ai-stream-simple-mock.ts rename to test/helpers/agents/llm-stream-simple-mock.ts index 070abc3d8cb..554e48a8bfd 100644 --- a/test/helpers/agents/pi-ai-stream-simple-mock.ts +++ b/test/helpers/agents/llm-stream-simple-mock.ts @@ -1,8 +1,8 @@ import { vi } from "vitest"; -type PiAiMockModule = Record; +type LlmMockModule = Record; -export function createPiAiStreamSimpleMock(): PiAiMockModule { +export function createLlmStreamSimpleMock(): LlmMockModule { return { streamSimple: vi.fn(() => ({ push: vi.fn(), diff --git a/test/helpers/agents/prompt-composition-scenarios.ts b/test/helpers/agents/prompt-composition-scenarios.ts index 4f506a51912..9ec5d1f84b8 100644 --- a/test/helpers/agents/prompt-composition-scenarios.ts +++ b/test/helpers/agents/prompt-composition-scenarios.ts @@ -7,10 +7,10 @@ import { buildBootstrapPromptWarning, } from "../../../src/agents/bootstrap-budget.js"; import { resolveBootstrapContextForRun } from "../../../src/agents/bootstrap-files.js"; -import { buildCurrentInboundPrompt } from "../../../src/agents/pi-embedded-runner/run/runtime-context-prompt.js"; -import { buildEmbeddedSystemPrompt } from "../../../src/agents/pi-embedded-runner/system-prompt.js"; +import { buildCurrentInboundPrompt } from "../../../src/agents/embedded-agent-runner/run/runtime-context-prompt.js"; +import { buildEmbeddedSystemPrompt } from "../../../src/agents/embedded-agent-runner/system-prompt.js"; import { buildAgentSystemPrompt } from "../../../src/agents/system-prompt.js"; -import { createStubTool } from "../../../src/agents/test-helpers/pi-tool-stubs.js"; +import { createStubTool } from "../../../src/agents/test-helpers/agent-tool-stubs.js"; import { buildDirectChatContext, buildGroupChatContext, diff --git a/test/helpers/auth-wizard.ts b/test/helpers/auth-wizard.ts index 9b58b4f6d3a..f1fac3bb231 100644 --- a/test/helpers/auth-wizard.ts +++ b/test/helpers/auth-wizard.ts @@ -47,7 +47,6 @@ export async function setupAuthTestEnv( const agentDir = path.join(stateDir, options?.agentSubdir ?? "agent"); process.env.OPENCLAW_STATE_DIR = stateDir; process.env.OPENCLAW_AGENT_DIR = agentDir; - process.env.PI_CODING_AGENT_DIR = agentDir; await fs.mkdir(agentDir, { recursive: true }); return { stateDir, agentDir }; } diff --git a/test/helpers/auto-reply/trigger-handling-test-harness.ts b/test/helpers/auto-reply/trigger-handling-test-harness.ts index 2aa3d65d2e6..5fc75593101 100644 --- a/test/helpers/auto-reply/trigger-handling-test-harness.ts +++ b/test/helpers/auto-reply/trigger-handling-test-harness.ts @@ -4,7 +4,7 @@ import os from "node:os"; import { join } from "node:path"; import { afterAll, afterEach, beforeAll, expect, vi } from "vitest"; import { clearRuntimeAuthProfileStoreSnapshots } from "../../../src/agents/auth-profiles.js"; -import type { EmbeddedPiQueueMessageOutcome } from "../../../src/agents/pi-embedded-runner/runs.js"; +import type { EmbeddedAgentQueueMessageOutcome } from "../../../src/agents/embedded-agent-runner/runs.js"; import { withFastReplyConfig } from "../../../src/auto-reply/reply/get-reply-fast-path.js"; import type { OpenClawConfig } from "../../../src/config/types.openclaw.js"; @@ -21,12 +21,12 @@ function getSharedMocks(key: string, create: () => T): T { return store[symbol]; } -const piEmbeddedMocks = getSharedMocks("openclaw.trigger-handling.pi-embedded-mocks", () => ({ - abortEmbeddedPiRun: vi.fn().mockReturnValue(false), - compactEmbeddedPiSession: vi.fn(), - runEmbeddedPiAgent: vi.fn(), - queueEmbeddedPiMessageWithOutcome: vi.fn( - (sessionId: string, _text?: string, _options?: unknown): EmbeddedPiQueueMessageOutcome => ({ +const embeddedAgentMocks = getSharedMocks("openclaw.trigger-handling.embedded-agent-mocks", () => ({ + abortEmbeddedAgentRun: vi.fn().mockReturnValue(false), + compactEmbeddedAgentSession: vi.fn(), + runEmbeddedAgent: vi.fn(), + queueEmbeddedAgentMessageWithOutcome: vi.fn( + (sessionId: string, _text?: string, _options?: unknown): EmbeddedAgentQueueMessageOutcome => ({ queued: false, sessionId, reason: "not_streaming", @@ -34,50 +34,52 @@ const piEmbeddedMocks = getSharedMocks("openclaw.trigger-handling.pi-embedded-mo }), ), resolveActiveEmbeddedRunSessionId: vi.fn().mockReturnValue(undefined), - isEmbeddedPiRunActive: vi.fn().mockReturnValue(false), - isEmbeddedPiRunStreaming: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunActive: vi.fn().mockReturnValue(false), + isEmbeddedAgentRunStreaming: vi.fn().mockReturnValue(false), })); -export function getAbortEmbeddedPiRunMock(): AnyMock { - return piEmbeddedMocks.abortEmbeddedPiRun; +export function getAbortEmbeddedAgentRunMock(): AnyMock { + return embeddedAgentMocks.abortEmbeddedAgentRun; } -export function getCompactEmbeddedPiSessionMock(): AnyMock { - return piEmbeddedMocks.compactEmbeddedPiSession; +export function getCompactEmbeddedAgentSessionMock(): AnyMock { + return embeddedAgentMocks.compactEmbeddedAgentSession; } -export function getRunEmbeddedPiAgentMock(): AnyMock { - return piEmbeddedMocks.runEmbeddedPiAgent; +export function getRunEmbeddedAgentMock(): AnyMock { + return embeddedAgentMocks.runEmbeddedAgent; } -const installPiEmbeddedMock = () => - vi.doMock("../../../src/agents/pi-embedded.js", () => ({ - abortEmbeddedPiRun: (...args: unknown[]) => piEmbeddedMocks.abortEmbeddedPiRun(...args), - compactEmbeddedPiSession: (...args: unknown[]) => - piEmbeddedMocks.compactEmbeddedPiSession(...args), - runEmbeddedPiAgent: (...args: unknown[]) => piEmbeddedMocks.runEmbeddedPiAgent(...args), - queueEmbeddedPiMessageWithOutcome: (sessionId: string, text: string, options?: unknown) => - piEmbeddedMocks.queueEmbeddedPiMessageWithOutcome(sessionId, text, options), +const installEmbeddedAgentMock = () => + vi.doMock("../../../src/agents/embedded-agent.js", () => ({ + abortEmbeddedAgentRun: (...args: unknown[]) => + embeddedAgentMocks.abortEmbeddedAgentRun(...args), + compactEmbeddedAgentSession: (...args: unknown[]) => + embeddedAgentMocks.compactEmbeddedAgentSession(...args), + runEmbeddedAgent: (...args: unknown[]) => embeddedAgentMocks.runEmbeddedAgent(...args), + queueEmbeddedAgentMessageWithOutcome: (sessionId: string, text: string, options?: unknown) => + embeddedAgentMocks.queueEmbeddedAgentMessageWithOutcome(sessionId, text, options), resolveEmbeddedSessionLane: (key: string) => `session:${key.trim() || "main"}`, resolveActiveEmbeddedRunSessionId: (...args: unknown[]) => - piEmbeddedMocks.resolveActiveEmbeddedRunSessionId(...args), - isEmbeddedPiRunActive: (...args: unknown[]) => piEmbeddedMocks.isEmbeddedPiRunActive(...args), - isEmbeddedPiRunStreaming: (...args: unknown[]) => - piEmbeddedMocks.isEmbeddedPiRunStreaming(...args), + embeddedAgentMocks.resolveActiveEmbeddedRunSessionId(...args), + isEmbeddedAgentRunActive: (...args: unknown[]) => + embeddedAgentMocks.isEmbeddedAgentRunActive(...args), + isEmbeddedAgentRunStreaming: (...args: unknown[]) => + embeddedAgentMocks.isEmbeddedAgentRunStreaming(...args), })); -installPiEmbeddedMock(); +installEmbeddedAgentMock(); -vi.doMock("../../../src/agents/pi-embedded-runner/runs.js", () => ({ - abortEmbeddedPiRun: (...args: unknown[]) => piEmbeddedMocks.abortEmbeddedPiRun(...args), - formatEmbeddedPiQueueFailureSummary: (outcome: { reason?: string; sessionId?: string }) => +vi.doMock("../../../src/agents/embedded-agent-runner/runs.js", () => ({ + abortEmbeddedAgentRun: (...args: unknown[]) => embeddedAgentMocks.abortEmbeddedAgentRun(...args), + formatEmbeddedAgentQueueFailureSummary: (outcome: { reason?: string; sessionId?: string }) => outcome.reason && outcome.sessionId ? `queue_message_failed reason=${outcome.reason} sessionId=${outcome.sessionId} gatewayHealth=live` : undefined, - queueEmbeddedPiMessageWithOutcome: (sessionId: string, text: string, options?: unknown) => - piEmbeddedMocks.queueEmbeddedPiMessageWithOutcome(sessionId, text, options), + queueEmbeddedAgentMessageWithOutcome: (sessionId: string, text: string, options?: unknown) => + embeddedAgentMocks.queueEmbeddedAgentMessageWithOutcome(sessionId, text, options), resolveActiveEmbeddedRunSessionId: (...args: unknown[]) => - piEmbeddedMocks.resolveActiveEmbeddedRunSessionId(...args), + embeddedAgentMocks.resolveActiveEmbeddedRunSessionId(...args), })); const providerUsageMocks = vi.hoisted(() => ({ @@ -264,10 +266,10 @@ export async function withTempHome(fn: (home: string) => Promise): Promise try { // Hard reset shared mocks so non-isolated runs don't inherit prior behavior. - piEmbeddedMocks.runEmbeddedPiAgent.mockReset(); - piEmbeddedMocks.abortEmbeddedPiRun.mockReset().mockReturnValue(false); - piEmbeddedMocks.compactEmbeddedPiSession.mockReset(); - piEmbeddedMocks.queueEmbeddedPiMessageWithOutcome + embeddedAgentMocks.runEmbeddedAgent.mockReset(); + embeddedAgentMocks.abortEmbeddedAgentRun.mockReset().mockReturnValue(false); + embeddedAgentMocks.compactEmbeddedAgentSession.mockReset(); + embeddedAgentMocks.queueEmbeddedAgentMessageWithOutcome .mockReset() .mockImplementation((sessionId: string) => ({ queued: false, @@ -275,8 +277,8 @@ export async function withTempHome(fn: (home: string) => Promise): Promise reason: "not_streaming", gatewayHealth: "live", })); - piEmbeddedMocks.isEmbeddedPiRunActive.mockReset().mockReturnValue(false); - piEmbeddedMocks.isEmbeddedPiRunStreaming.mockReset().mockReturnValue(false); + embeddedAgentMocks.isEmbeddedAgentRunActive.mockReset().mockReturnValue(false); + embeddedAgentMocks.isEmbeddedAgentRunStreaming.mockReset().mockReturnValue(false); modelFallbackMocks.runWithModelFallback.mockClear(); return await fn(home); } finally { @@ -341,8 +343,8 @@ export async function expectInlineCommandHandledAndStripped(params: { blockReplyContains: string; requestOverrides?: Record; }) { - const runEmbeddedPiAgentMock = mockRunEmbeddedPiAgentOk(); - runEmbeddedPiAgentMock.mockClear(); + const runEmbeddedAgentMock = mockRunEmbeddedAgentOk(); + runEmbeddedAgentMock.mockClear(); const { blockReplies, handlers } = createBlockReplyCollector(); const res = await params.getReplyFromConfig( { @@ -359,8 +361,8 @@ export async function expectInlineCommandHandledAndStripped(params: { const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(blockReplies.length).toBe(1); expect(blockReplies[0]?.text).toContain(params.blockReplyContains); - expect(runEmbeddedPiAgentMock).toHaveBeenCalled(); - const lastCall = runEmbeddedPiAgentMock.mock.calls[runEmbeddedPiAgentMock.mock.calls.length - 1]; + expect(runEmbeddedAgentMock).toHaveBeenCalled(); + const lastCall = runEmbeddedAgentMock.mock.calls[runEmbeddedAgentMock.mock.calls.length - 1]; const prompt = lastCall?.[0]?.prompt ?? ""; expect(prompt).not.toContain(params.stripToken); expect(text).toBe("ok"); @@ -371,9 +373,9 @@ export async function expectBareNewOrResetAcknowledged(params: { body: "/new" | "/reset"; getReplyFromConfig: typeof import("../../../src/auto-reply/reply.js").getReplyFromConfig; }) { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockClear(); - runEmbeddedPiAgentMock.mockResolvedValue({ + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); + runEmbeddedAgentMock.mockClear(); + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text: "hello" }], meta: { durationMs: 1, @@ -393,7 +395,7 @@ export async function expectBareNewOrResetAcknowledged(params: { ); const text = Array.isArray(res) ? res[0]?.text : res?.text; expect(text).toBe(params.body === "/reset" ? "✅ Session reset." : "✅ New session started."); - expect(runEmbeddedPiAgentMock).not.toHaveBeenCalled(); + expect(runEmbeddedAgentMock).not.toHaveBeenCalled(); } export function installTriggerHandlingE2eTestHooks() { @@ -403,16 +405,16 @@ export function installTriggerHandlingE2eTestHooks() { }); } -export function mockRunEmbeddedPiAgentOk(text = "ok"): AnyMock { - const runEmbeddedPiAgentMock = getRunEmbeddedPiAgentMock(); - runEmbeddedPiAgentMock.mockResolvedValue({ +export function mockRunEmbeddedAgentOk(text = "ok"): AnyMock { + const runEmbeddedAgentMock = getRunEmbeddedAgentMock(); + runEmbeddedAgentMock.mockResolvedValue({ payloads: [{ text }], meta: { durationMs: 1, agentMeta: { sessionId: "s", provider: "p", model: "m" }, }, }); - return runEmbeddedPiAgentMock; + return runEmbeddedAgentMock; } export function createBlockReplyCollector() { diff --git a/test/helpers/config/heartbeat-config-honor.inventory.ts b/test/helpers/config/heartbeat-config-honor.inventory.ts index c8eef3b1ce5..3e5c5f51c1b 100644 --- a/test/helpers/config/heartbeat-config-honor.inventory.ts +++ b/test/helpers/config/heartbeat-config-honor.inventory.ts @@ -49,7 +49,7 @@ export const HEARTBEAT_CONFIG_HONOR_INVENTORY: ConfigHonorInventoryRow[] = [ mergePaths: ["src/agents/heartbeat-system-prompt.ts"], consumerPaths: [ "src/agents/heartbeat-system-prompt.ts", - "src/agents/pi-embedded-runner/run/attempt.prompt-helpers.ts", + "src/agents/embedded-agent-runner/run/attempt.prompt-helpers.ts", ], reloadPaths: ["src/gateway/config-reload-plan.ts"], testPaths: ["src/agents/heartbeat-system-prompt.test.ts"], diff --git a/test/image-generation.runtime.live.test.ts b/test/image-generation.runtime.live.test.ts index cb5b59c30a5..76d31a533ab 100644 --- a/test/image-generation.runtime.live.test.ts +++ b/test/image-generation.runtime.live.test.ts @@ -4,10 +4,10 @@ import { } from "openclaw/plugin-sdk/plugin-test-runtime"; import { describe, expect, it } from "vitest"; import { resolveDefaultAgentDir } from "../src/agents/agent-scope.js"; +import { isBillingErrorMessage } from "../src/agents/embedded-agent-helpers/failover-matches.js"; import { collectProviderApiKeys } from "../src/agents/live-auth-keys.js"; import { isLiveProfileKeyModeEnabled, isLiveTestEnabled } from "../src/agents/live-test-helpers.js"; import { resolveApiKeyForProvider } from "../src/agents/model-auth.js"; -import { isBillingErrorMessage } from "../src/agents/pi-embedded-helpers/failover-matches.js"; import { loadConfig, type OpenClawConfig } from "../src/config/config.js"; import { DEFAULT_LIVE_IMAGE_MODELS, diff --git a/test/non-isolated-runner.ts b/test/non-isolated-runner.ts index 89ef4d259ac..675ce56472f 100644 --- a/test/non-isolated-runner.ts +++ b/test/non-isolated-runner.ts @@ -53,7 +53,6 @@ function restoreSharedTestHomeAfterEnvUnstub(testHomeRaw: string | undefined): v delete process.env.OPENCLAW_CONFIG_PATH; delete process.env.OPENCLAW_STATE_DIR; delete process.env.OPENCLAW_AGENT_DIR; - delete process.env.PI_CODING_AGENT_DIR; process.env.XDG_CONFIG_HOME = path.join(testHome, ".config"); process.env.XDG_DATA_HOME = path.join(testHome, ".local", "share"); process.env.XDG_STATE_HOME = path.join(testHome, ".local", "state"); diff --git a/test/scripts/ci-node-test-plan.test.ts b/test/scripts/ci-node-test-plan.test.ts index 95d5d3bcc16..9396c0113eb 100644 --- a/test/scripts/ci-node-test-plan.test.ts +++ b/test/scripts/ci-node-test-plan.test.ts @@ -353,7 +353,7 @@ describe("scripts/lib/ci-node-test-plan.mjs", () => { shardName: "agentic-agents", configs: [ "test/vitest/vitest.agents-core.config.ts", - "test/vitest/vitest.agents-pi-embedded.config.ts", + "test/vitest/vitest.agents-embedded-agent.config.ts", "test/vitest/vitest.agents-support.config.ts", "test/vitest/vitest.agents-tools.config.ts", ], diff --git a/test/scripts/docker-e2e-plan.test.ts b/test/scripts/docker-e2e-plan.test.ts index 7940c2c6ae7..07893e48907 100644 --- a/test/scripts/docker-e2e-plan.test.ts +++ b/test/scripts/docker-e2e-plan.test.ts @@ -851,7 +851,7 @@ describe("scripts/lib/docker-e2e-plan", () => { "openai-web-search-minimal", "mcp-channels", "cron-mcp-cleanup", - "pi-bundle-mcp-tools", + "agent-bundle-mcp-tools", "crestodian-first-run", "crestodian-planner", "crestodian-rescue", @@ -878,7 +878,7 @@ describe("scripts/lib/docker-e2e-plan", () => { { name: "openai-web-search-minimal", stateScenario: "empty" }, { name: "mcp-channels", stateScenario: "empty" }, { name: "cron-mcp-cleanup", stateScenario: "empty" }, - { name: "pi-bundle-mcp-tools", stateScenario: "empty" }, + { name: "agent-bundle-mcp-tools", stateScenario: "empty" }, { name: "crestodian-first-run", stateScenario: "empty" }, { name: "crestodian-planner", stateScenario: "empty" }, { name: "crestodian-rescue", stateScenario: "empty" }, diff --git a/test/scripts/generate-npm-shrinkwrap.test.ts b/test/scripts/generate-npm-shrinkwrap.test.ts index 1d9e502c1af..e1c05450ff2 100644 --- a/test/scripts/generate-npm-shrinkwrap.test.ts +++ b/test/scripts/generate-npm-shrinkwrap.test.ts @@ -57,17 +57,15 @@ describe("generate-npm-shrinkwrap", () => { it("parses nested scoped package paths", () => { expect( - parseLockPackagePath( - "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk", - ), + parseLockPackagePath("node_modules/@openclaw/codex/node_modules/@anthropic-ai/sdk"), ).toEqual([ { - name: "@earendil-works/pi-coding-agent", - path: "node_modules/@earendil-works/pi-coding-agent", + name: "@openclaw/codex", + path: "node_modules/@openclaw/codex", }, { name: "@anthropic-ai/sdk", - path: "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk", + path: "node_modules/@openclaw/codex/node_modules/@anthropic-ai/sdk", }, ]); }); @@ -88,20 +86,19 @@ describe("generate-npm-shrinkwrap", () => { const lockfile = { packages: { "": {}, - "node_modules/@earendil-works/pi-coding-agent": { + "node_modules/@openclaw/codex": { version: "0.75.4", hasShrinkwrap: true, }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { + "node_modules/@openclaw/codex/node_modules/protobufjs": { version: "7.5.9", }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob": { + "node_modules/@openclaw/codex/node_modules/fetch-blob": { version: "4.0.0", }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob/node_modules/node-domexception": - { - version: "1.0.0", - }, + "node_modules/@openclaw/codex/node_modules/fetch-blob/node_modules/node-domexception": { + version: "1.0.0", + }, }, }; const overrideRules = exactOverrideRulesFromOverrides({ @@ -111,13 +108,11 @@ describe("generate-npm-shrinkwrap", () => { expect(collectOverrideViolations(lockfile, overrideRules)).toHaveLength(2); expect(disableShrinkwrappedOverrideConflictSources(lockfile, overrideRules)).toEqual([ - "node_modules/@earendil-works/pi-coding-agent", + "node_modules/@openclaw/codex", ]); - expect(lockfile.packages["node_modules/@earendil-works/pi-coding-agent"]).not.toHaveProperty( - "hasShrinkwrap", - ); + expect(lockfile.packages["node_modules/@openclaw/codex"]).not.toHaveProperty("hasShrinkwrap"); expect( - lockfile.packages["node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs"], + lockfile.packages["node_modules/@openclaw/codex/node_modules/protobufjs"], ).toBeUndefined(); }); diff --git a/test/scripts/lint-suppressions.test.ts b/test/scripts/lint-suppressions.test.ts index e5871311f8f..1121e0313ef 100644 --- a/test/scripts/lint-suppressions.test.ts +++ b/test/scripts/lint-suppressions.test.ts @@ -132,7 +132,7 @@ describe("production lint suppressions", () => { "scripts/lib/plugin-npm-release.ts|typescript/no-unnecessary-type-parameters|1", "src/agents/agent-scope.ts|no-control-regex|1", "src/agents/code-mode.worker.ts|unicorn/require-post-message-target-origin|1", - "src/agents/pi-embedded-runner/run/images.ts|no-control-regex|1", + "src/agents/embedded-agent-runner/run/images.ts|no-control-regex|1", "src/agents/subagent-attachments.ts|no-control-regex|1", "src/agents/subagent-spawn.ts|no-control-regex|1", "src/channels/plugins/channel-runtime-surface.types.ts|typescript/no-unnecessary-type-parameters|1", diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index a86ef547127..97e07536ee8 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -393,7 +393,7 @@ describe("scripts/openclaw-cross-os-release-checks", () => { expect(CROSS_OS_AGENT_TURN_TIMEOUT_SECONDS).toBeGreaterThanOrEqual(600); expect(source).toContain("buildReleaseProviderConfigOverride"); expect(source).toContain("models: []"); - expect(source).toContain('agentRuntime: { id: "pi" }'); + expect(source).toContain('agentRuntime: { id: "openclaw" }'); expect(source).toContain('"--merge"'); expect(source).toContain(providerOverride); expect(source).not.toContain("models.providers.${params.providerConfig.extensionId}.baseUrl"); diff --git a/test/scripts/parallels-smoke-model.test.ts b/test/scripts/parallels-smoke-model.test.ts index d3973608894..3087ba774d1 100644 --- a/test/scripts/parallels-smoke-model.test.ts +++ b/test/scripts/parallels-smoke-model.test.ts @@ -758,7 +758,7 @@ if (isPrlctl) { expect(powershell).toContain("models.providers.${providerId}"); expect(powershell).toContain("agents.defaults.models${configPathMapKey(modelId)}"); expect(powershell).toContain("OPENCLAW_PARALLELS_AGENT_RUNTIME_POLICY_SUPPORTED"); - expect(powershell).toContain('selectedModelEntry.agentRuntime = { id: "pi" }'); + expect(powershell).toContain('selectedModelEntry.agentRuntime = { id: "openclaw" }'); expect(powershell).toContain("delete selectedModelEntry.agentRuntime"); expect(powershell).toContain("delete providerEntry.agentRuntime"); expect(powershell).toContain("configPathMapKey"); diff --git a/test/scripts/root-package-overrides.test.ts b/test/scripts/root-package-overrides.test.ts index 3c13ea477b4..02fd7fa21c2 100644 --- a/test/scripts/root-package-overrides.test.ts +++ b/test/scripts/root-package-overrides.test.ts @@ -36,12 +36,11 @@ describe("root package override guardrails", () => { ); const bedrockRuntimeDependency = bedrockManifest.dependencies?.[packageName]; const npmOverride = manifest.overrides?.[packageName]; - const pnpmOverride = pnpmWorkspace.overrides?.[packageName]; expect(bedrockRuntimeDependency).toBeDefined(); expect(manifest.dependencies).not.toHaveProperty(packageName); expect(npmOverride).toBeUndefined(); - expect(pnpmOverride).toBe(bedrockRuntimeDependency); + expect(pnpmWorkspace.overrides).not.toHaveProperty(packageName); }); it("pins the node-domexception alias exactly in npm and pnpm overrides", () => { diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 85477247232..7c1f7da387b 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -1410,7 +1410,7 @@ describe("scripts/test-projects full-suite sharding", () => { "test/vitest/vitest.commands-light.config.ts", "test/vitest/vitest.commands.config.ts", "test/vitest/vitest.agents-core.config.ts", - "test/vitest/vitest.agents-pi-embedded.config.ts", + "test/vitest/vitest.agents-embedded-agent.config.ts", "test/vitest/vitest.agents-support.config.ts", "test/vitest/vitest.agents-tools.config.ts", "test/vitest/vitest.daemon.config.ts", diff --git a/test/scripts/transitive-manifest-risk-report.test.ts b/test/scripts/transitive-manifest-risk-report.test.ts index 3e4dc3597ef..2201602f62b 100644 --- a/test/scripts/transitive-manifest-risk-report.test.ts +++ b/test/scripts/transitive-manifest-risk-report.test.ts @@ -118,7 +118,7 @@ describe("transitive-manifest-risk-report", () => { it("documents JSON completeness and renders grouped Markdown summaries", async () => { const report = await createTransitiveManifestRiskReport({ packageVersions: [ - { packageName: "@earendil-works/pi-ai", version: "0.74.0" }, + { packageName: "openclaw/plugin-sdk/llm", version: "0.74.0" }, { packageName: "aaa-package", version: "1.0.0" }, { packageName: "recent-package", version: "1.0.0" }, ], @@ -129,7 +129,7 @@ describe("transitive-manifest-risk-report", () => { publishedAt: packageName === "recent-package" ? "2026-05-11T23:00:00Z" : "2026-04-01T00:00:00Z", manifest: - packageName === "@earendil-works/pi-ai" + packageName === "openclaw/plugin-sdk/llm" ? { dependencies: { "@mistralai/mistralai": "^2.2.0", @@ -164,7 +164,7 @@ describe("transitive-manifest-risk-report", () => { expect(markdown).toContain("## Complete Evidence"); expect(markdown).toContain("The complete reported signal list is available in the JSON report"); expect(markdown).toContain("## Published Package Manifests With Risk Findings"); - expect(markdown).toContain("`@earendil-works/pi-ai@0.74.0`: 1 manifest finding"); + expect(markdown).toContain("`openclaw/plugin-sdk/llm@0.74.0`: 1 manifest finding"); expect(markdown).toContain("`aaa-package@1.0.0`: 1 manifest finding"); expect(markdown).toContain("## Floating Dependency Targets"); expect(markdown).toContain("`@mistralai/mistralai`: 1 declarations"); diff --git a/test/setup.shared.ts b/test/setup.shared.ts index 1b3e262fd7f..a16320f3f2e 100644 --- a/test/setup.shared.ts +++ b/test/setup.shared.ts @@ -5,7 +5,7 @@ type GlobalWithOpenAiCodexTokenRefreshTestHook = typeof globalThis & { [openAiCodexTokenRefreshTestHook]?: ((...args: unknown[]) => unknown) | undefined; }; -vi.mock("@earendil-works/pi-ai/oauth", () => ({ +vi.mock("openclaw/plugin-sdk/llm-oauth", () => ({ getOAuthProvider: () => undefined, getOAuthApiKey: () => undefined, getOAuthProviders: () => [], diff --git a/test/test-env.ts b/test/test-env.ts index 91bf9497f6e..43110a18be8 100644 --- a/test/test-env.ts +++ b/test/test-env.ts @@ -174,7 +174,6 @@ function resolveRestoreEntries(): RestoreEntry[] { { key: "OPENCLAW_CANVAS_HOST_PORT", value: process.env.OPENCLAW_CANVAS_HOST_PORT }, { key: "OPENCLAW_TEST_HOME", value: process.env.OPENCLAW_TEST_HOME }, { key: "OPENCLAW_AGENT_DIR", value: process.env.OPENCLAW_AGENT_DIR }, - { key: "PI_CODING_AGENT_DIR", value: process.env.PI_CODING_AGENT_DIR }, { key: "TELEGRAM_BOT_TOKEN", value: process.env.TELEGRAM_BOT_TOKEN }, { key: "DISCORD_BOT_TOKEN", value: process.env.DISCORD_BOT_TOKEN }, { key: "SLACK_BOT_TOKEN", value: process.env.SLACK_BOT_TOKEN }, @@ -205,7 +204,6 @@ function createIsolatedTestHome(restore: RestoreEntry[]): { // Prefer deriving state dir from HOME so nested tests that change HOME also isolate correctly. delete process.env.OPENCLAW_STATE_DIR; delete process.env.OPENCLAW_AGENT_DIR; - delete process.env.PI_CODING_AGENT_DIR; // Prefer test-controlled ports over developer overrides (avoid port collisions across tests/workers). delete process.env.OPENCLAW_GATEWAY_PORT; delete process.env.OPENCLAW_BRIDGE_ENABLED; diff --git a/test/vitest-projects-config.test.ts b/test/vitest-projects-config.test.ts index f4bf0e3e1ad..c55776352cd 100644 --- a/test/vitest-projects-config.test.ts +++ b/test/vitest-projects-config.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it } from "vitest"; import { createPatternFileHelper } from "./helpers/pattern-file.js"; import { normalizeConfigPath, normalizeConfigPaths } from "./helpers/vitest-config-paths.js"; import { createAgentsCoreVitestConfig } from "./vitest/vitest.agents-core.config.ts"; -import { createAgentsPiEmbeddedVitestConfig } from "./vitest/vitest.agents-pi-embedded.config.ts"; +import { createAgentsEmbeddedVitestConfig } from "./vitest/vitest.agents-embedded-agent.config.ts"; import { createAgentsSupportVitestConfig } from "./vitest/vitest.agents-support.config.ts"; import { createAgentsToolsVitestConfig } from "./vitest/vitest.agents-tools.config.ts"; import { createAgentsVitestConfig } from "./vitest/vitest.agents.config.ts"; @@ -92,7 +92,7 @@ describe("projects vitest config", () => { expect(createGatewayVitestConfig().test.pool).toBe("threads"); expect(createAgentsVitestConfig().test.pool).toBe("threads"); expect(createAgentsCoreVitestConfig().test.pool).toBe("threads"); - expect(createAgentsPiEmbeddedVitestConfig().test.pool).toBe("threads"); + expect(createAgentsEmbeddedVitestConfig().test.pool).toBe("threads"); expect(createAgentsSupportVitestConfig().test.pool).toBe("threads"); expect(createAgentsToolsVitestConfig().test.pool).toBe("threads"); expect(createCommandsLightVitestConfig().test.pool).toBe("threads"); diff --git a/test/vitest-unit-fast-config.test.ts b/test/vitest-unit-fast-config.test.ts index f79784ceb27..c806156511f 100644 --- a/test/vitest-unit-fast-config.test.ts +++ b/test/vitest-unit-fast-config.test.ts @@ -100,7 +100,9 @@ describe("unit-fast vitest lane", () => { expect(testConfig.isolate).toBe(false); expect(testConfig.runner).toBeUndefined(); expect(testConfig.setupFiles).toStrictEqual([]); - expect(testConfig.include).toContain("src/agents/pi-tools.deferred-followup-guidance.test.ts"); + expect(testConfig.include).toContain( + "src/agents/agent-tools.deferred-followup-guidance.test.ts", + ); expect(testConfig.include).toContain("src/acp/control-plane/runtime-cache.test.ts"); expect(testConfig.include).toContain("src/acp/runtime/registry.test.ts"); expect(testConfig.include).toContain("src/commands/status-overview-values.test.ts"); diff --git a/test/vitest-unit-paths.test.ts b/test/vitest-unit-paths.test.ts index 9d200fb3d22..f9b8131bc9d 100644 --- a/test/vitest-unit-paths.test.ts +++ b/test/vitest-unit-paths.test.ts @@ -29,7 +29,7 @@ describe("isUnitConfigTestFile", () => { expect(isUnitConfigTestFile("src/infra/stable-node-path.test.ts")).toBe(false); expect(isUnitConfigTestFile("test/format-error.test.ts")).toBe(false); expect(isUnitConfigTestFile("test/extension-test-boundary.test.ts")).toBe(false); - expect(isUnitConfigTestFile("src/agents/pi-embedded-runner.test.ts")).toBe(false); + expect(isUnitConfigTestFile("src/agents/embedded-agent-runner.test.ts")).toBe(false); expect(isUnitConfigTestFile("src/commands/onboard.test.ts")).toBe(false); expect(isUnitConfigTestFile("ui/src/ui/views/channels.test.ts")).toBe(false); expect(isUnitConfigTestFile("ui/src/ui/views/chat.test.ts")).toBe(false); diff --git a/test/vitest/vitest.agents-embedded-agent.config.ts b/test/vitest/vitest.agents-embedded-agent.config.ts new file mode 100644 index 00000000000..60ce779aa16 --- /dev/null +++ b/test/vitest/vitest.agents-embedded-agent.config.ts @@ -0,0 +1,12 @@ +import { agentsEmbeddedTestPatterns } from "./vitest.agents-paths.mjs"; +import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; + +export function createAgentsEmbeddedVitestConfig(env?: Record) { + return createScopedVitestConfig(agentsEmbeddedTestPatterns, { + dir: "src/agents", + env, + name: "agents-embedded-agent", + }); +} + +export default createAgentsEmbeddedVitestConfig(); diff --git a/test/vitest/vitest.agents-paths.mjs b/test/vitest/vitest.agents-paths.mjs index 113ad2ee9ad..0a4a9299d91 100644 --- a/test/vitest/vitest.agents-paths.mjs +++ b/test/vitest/vitest.agents-paths.mjs @@ -2,13 +2,13 @@ export const agentsAllTestPatterns = ["src/agents/**/*.test.ts"]; export const agentsCoreTestPatterns = ["src/agents/*.test.ts"]; -export const agentsPiEmbeddedTestPatterns = ["src/agents/pi-embedded-runner/**/*.test.ts"]; +export const agentsEmbeddedTestPatterns = ["src/agents/embedded-agent-runner/**/*.test.ts"]; export const agentsToolsTestPatterns = ["src/agents/tools/**/*.test.ts"]; export const agentsSupportTestPatterns = ["src/agents/*/**/*.test.ts"]; export const agentsSupportExcludePatterns = [ - "src/agents/pi-embedded-runner/**", + "src/agents/embedded-agent-runner/**", "src/agents/tools/**", ]; diff --git a/test/vitest/vitest.agents-pi-embedded.config.ts b/test/vitest/vitest.agents-pi-embedded.config.ts deleted file mode 100644 index 0738c1aeaf1..00000000000 --- a/test/vitest/vitest.agents-pi-embedded.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { agentsPiEmbeddedTestPatterns } from "./vitest.agents-paths.mjs"; -import { createScopedVitestConfig } from "./vitest.scoped-config.ts"; - -export function createAgentsPiEmbeddedVitestConfig(env?: Record) { - return createScopedVitestConfig(agentsPiEmbeddedTestPatterns, { - dir: "src/agents", - env, - name: "agents-pi-embedded", - }); -} - -export default createAgentsPiEmbeddedVitestConfig(); diff --git a/test/vitest/vitest.config.ts b/test/vitest/vitest.config.ts index 48a5828f20f..c3b4e57ba59 100644 --- a/test/vitest/vitest.config.ts +++ b/test/vitest/vitest.config.ts @@ -33,7 +33,7 @@ export const rootVitestProjects = [ "test/vitest/vitest.commands.config.ts", "test/vitest/vitest.auto-reply.config.ts", "test/vitest/vitest.agents-core.config.ts", - "test/vitest/vitest.agents-pi-embedded.config.ts", + "test/vitest/vitest.agents-embedded-agent.config.ts", "test/vitest/vitest.agents-support.config.ts", "test/vitest/vitest.agents-tools.config.ts", "test/vitest/vitest.daemon.config.ts", diff --git a/test/vitest/vitest.scoped-config.ts b/test/vitest/vitest.scoped-config.ts index 5e115695284..e72cecf39a5 100644 --- a/test/vitest/vitest.scoped-config.ts +++ b/test/vitest/vitest.scoped-config.ts @@ -98,7 +98,7 @@ const SCOPED_PROJECT_GROUP_ORDER_BY_NAME = new Map( "acp", "agents", "agents-core", - "agents-pi-embedded", + "agents-embedded-agent", "agents-support", "agents-tools", "auto-reply", diff --git a/test/vitest/vitest.shared.config.ts b/test/vitest/vitest.shared.config.ts index 30cc801a3be..c6e27a9d783 100644 --- a/test/vitest/vitest.shared.config.ts +++ b/test/vitest/vitest.shared.config.ts @@ -208,7 +208,7 @@ export const sharedVitestConfig = { "test/vitest/vitest.channel-paths.mjs", "test/vitest/vitest.agents-paths.mjs", "test/vitest/vitest.agents-core.config.ts", - "test/vitest/vitest.agents-pi-embedded.config.ts", + "test/vitest/vitest.agents-embedded-agent.config.ts", "test/vitest/vitest.agents-support.config.ts", "test/vitest/vitest.agents-tools.config.ts", "test/vitest/vitest.channels.config.ts", @@ -371,11 +371,11 @@ export const sharedVitestConfig = { "src/providers/**", "src/secrets/**", "src/agents/model-scan.ts", - "src/agents/pi-embedded-runner.ts", + "src/agents/embedded-agent-runner.ts", "src/agents/sandbox-paths.ts", "src/agents/sandbox.ts", "src/agents/skills-install.ts", - "src/agents/pi-tool-definition-adapter.ts", + "src/agents/agent-tool-definition-adapter.ts", "src/agents/tools/discord-actions*.ts", "src/agents/tools/slack-actions.ts", "src/infra/state-migrations.ts", diff --git a/test/vitest/vitest.test-shards.mjs b/test/vitest/vitest.test-shards.mjs index fe19d75b4a3..fc4896030d8 100644 --- a/test/vitest/vitest.test-shards.mjs +++ b/test/vitest/vitest.test-shards.mjs @@ -88,7 +88,7 @@ export const fullSuiteVitestShards = [ "test/vitest/vitest.commands-light.config.ts", "test/vitest/vitest.commands.config.ts", "test/vitest/vitest.agents-core.config.ts", - "test/vitest/vitest.agents-pi-embedded.config.ts", + "test/vitest/vitest.agents-embedded-agent.config.ts", "test/vitest/vitest.agents-support.config.ts", "test/vitest/vitest.agents-tools.config.ts", "test/vitest/vitest.daemon.config.ts",