[codex] Add heartbeat policy eval coverage (#9087)
## Thinking Path > - Paperclip is the open source control plane people use to manage AI agents for work. > - The relevant subsystem is the agent heartbeat policy surface: the Paperclip skill, default onboarding AGENTS.md, new-agent runtime defaults, and promptfoo eval coverage for agent behavior. > - A broad recovery PR collected several unrelated local-mainline changes, which made review too large and mixed policy/eval updates with server execution and UI work. > - This PR extracts only the heartbeat policy and prompt-eval slice so reviewers can assess the behavior contract independently. > - The eval additions cover scoped wake handling, idle no-op behavior, dependency-blocked comment triage, final disposition, budget hard stops, and Phase 5 memory/control-surface policy expectations. > - The benefit is a narrower review surface plus deterministic follow-up guidance for server/shared tests that should back these prompt-level checks. ## Linked Issues or Issue Description Refs #8866 No public issue was filed for this split. This is a focused extraction from the closed broad recovery PR so heartbeat policy and eval coverage can be reviewed separately from execution behavior, work-product feature work, plugin hardening, pipeline health, and unrelated UI polish. ## What Changed - Added promptfoo release-gate cases for scoped wake payload handling, idle exits, dependency-blocked comment triage, final disposition, and budget hard-stop behavior. - Added Phase 5 memory/control-surface prompt eval cases for provider binding precedence, provenance/audit fields, hook cost/trust handling, and auditable board command surfaces. - Documented how these prompt evals map to deterministic server/shared follow-up coverage. - Updated agent policy guidance so operator-facing engineering outputs such as PRs, branches, commits, previews, and runtime services get matching work products. - Defaulted new agent runtime config to skip timer heartbeats when there is no actionable work, with focused test coverage. ## Verification - `cd evals/promptfoo && npx promptfoo@latest validate -c promptfooconfig.yaml` passes. - `/srv/paperclip/home/paperclipai/paperclip/node_modules/.bin/vitest run ui/src/lib/new-agent-runtime-config.test.ts` passes in an isolated worktree after `pnpm install --ignore-scripts --frozen-lockfile` created workspace links. - A live promptfoo eval was not run because `OPENROUTER_API_KEY`, `OPENAI_API_KEY`, and `ANTHROPIC_API_KEY` were unset in the workspace. ## Risks Low-to-medium risk. The runtime default reduces timer-driven empty heartbeats for newly created agents, so the main behavioral risk is missing an edge case where timer wakes were expected despite no actionable work. The promptfoo additions are deterministic assertion coverage and documentation-only until a live eval is run with provider credentials. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI GPT-5-based Codex coding agent in the Paperclip local Codex adapter environment; exact hosted model ID and context window were not exposed to the agent runtime. Tool use included shell, git, promptfoo validation, Vitest, and the GitHub connector/CLI. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
903886bc79
commit
dfc256a543
|
|
@ -26,6 +26,9 @@ export OPENAI_API_KEY=sk-... # OpenAI direct
|
|||
# Smoke test (default models)
|
||||
pnpm evals:smoke
|
||||
|
||||
# Validate config without provider credentials
|
||||
cd evals/promptfoo && npx promptfoo@latest validate -c promptfooconfig.yaml
|
||||
|
||||
# Or run promptfoo directly
|
||||
cd evals/promptfoo
|
||||
promptfoo eval
|
||||
|
|
@ -48,11 +51,22 @@ Phase 0 covers narrow behavior evals for the Paperclip heartbeat skill:
|
|||
| No work exit | `core` | Agent exits cleanly with no assignments |
|
||||
| Checkout before work | `core` | Agent always checks out before modifying |
|
||||
| 409 conflict handling | `core` | Agent stops on 409, picks different task |
|
||||
| Memory provider binding | `phase5_memory` | Agent honors agent override before company default |
|
||||
| Memory provenance audit | `phase5_memory` | Agent preserves inspectable source and operation records |
|
||||
| Memory hook cost/trust | `phase5_memory` | Agent keeps memory hook cost attribution and source trust visible |
|
||||
| Board command work objects | `phase5_control_surface` | Chat-like board commands create auditable work objects |
|
||||
|
||||
Phase 5 memory/control-surface prompt evals should be paired with deterministic server/shared tests for:
|
||||
|
||||
- memory provider resolution order: agent override, then company default
|
||||
- memory operation audit rows including company, agent, issue, run, provider, source, and cost references
|
||||
- hook-delivered memory payloads preserving source trust and cost attribution fields
|
||||
- board command/chat-like routes creating auditable issues, comments, documents, approvals, or work products
|
||||
|
||||
### Adding new cases
|
||||
|
||||
1. Add a YAML file to `evals/promptfoo/cases/`
|
||||
2. Follow the existing case format (see `core-assignment-pickup.yaml` for reference)
|
||||
1. Add a YAML file to `evals/promptfoo/tests/`
|
||||
2. Follow the existing case format (see `core.yaml` for reference)
|
||||
3. Run `promptfoo eval` to test
|
||||
|
||||
### Phases
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json
|
||||
# Paperclip Agent Evals - Phase 0: Promptfoo Bootstrap
|
||||
#
|
||||
# Tests narrow heartbeat behaviors across models with deterministic assertions.
|
||||
|
|
@ -9,7 +10,7 @@
|
|||
# promptfoo view # open results in browser
|
||||
#
|
||||
# Validate config before committing:
|
||||
# promptfoo validate
|
||||
# npx promptfoo@latest validate -c promptfooconfig.yaml
|
||||
#
|
||||
# Requires OPENROUTER_API_KEY or individual provider keys.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
# Phase 5 memory and control-surface prompt behavior tests.
|
||||
# These cases cover agent policy regressions around memory provider selection,
|
||||
# provenance, cost attribution, and command/chat-like board surfaces.
|
||||
#
|
||||
# Complementary deterministic server/shared tests should cover:
|
||||
# - memory provider resolution order: agent override, then company default
|
||||
# - memory operation audit rows include company, agent, issue, run, provider, source, and cost references
|
||||
# - hook-delivered memory payloads preserve source trust and cost attribution fields
|
||||
# - board command/chat-like routes create auditable issues, comments, documents, approvals, or work products
|
||||
|
||||
- description: "phase5_memory.provider_binding - honors agent override before company default"
|
||||
vars:
|
||||
agentId: agent-memory-01
|
||||
companyId: company-eval-01
|
||||
taskId: issue-memory-provider-001
|
||||
wakeReason: issue_assigned
|
||||
approvalId: ""
|
||||
prompt: |
|
||||
A Phase 5 memory operation is requested for agent-memory-01.
|
||||
Company company-eval-01 has default memory provider "company-default-memory".
|
||||
agent-memory-01 has an override provider "agent-override-memory".
|
||||
|
||||
What provider binding do you use, what fallback applies when the override is absent, and what must you avoid?
|
||||
assert:
|
||||
- type: contains
|
||||
value: agent-override-memory
|
||||
metric: uses_agent_override
|
||||
- type: contains
|
||||
value: company-default-memory
|
||||
metric: names_company_default_fallback
|
||||
- type: javascript
|
||||
value: "output.match(/override.*(first|before|wins)|agent.*override/i) !== null"
|
||||
metric: override_precedence
|
||||
- type: javascript
|
||||
value: "!output.match(/global default|hard.?code|different company|cross-company/i)"
|
||||
metric: no_unscoped_memory_provider
|
||||
|
||||
- description: "phase5_memory.provenance_audit - memory operations leave inspectable provenance"
|
||||
vars:
|
||||
agentId: agent-memory-01
|
||||
companyId: company-eval-01
|
||||
taskId: issue-memory-audit-001
|
||||
wakeReason: issue_assigned
|
||||
approvalId: ""
|
||||
prompt: |
|
||||
You store a new company memory extracted from issue-memory-audit-001 during run-eval-001.
|
||||
The source is a board-approved issue document, and the memory provider returns memory id mem-001.
|
||||
|
||||
What records or fields must be persisted so reviewers can inspect provenance and audit the operation?
|
||||
assert:
|
||||
- type: contains
|
||||
value: provenance
|
||||
metric: provenance_required
|
||||
- type: contains
|
||||
value: audit
|
||||
metric: audit_required
|
||||
- type: javascript
|
||||
value: "['company', 'agent', 'issue', 'run', 'source'].every((term) => output.toLowerCase().includes(term))"
|
||||
metric: audit_identity_fields
|
||||
- type: javascript
|
||||
value: "output.includes('mem-001') || output.match(/memory id|memory_id|provider id/i) !== null"
|
||||
metric: provider_memory_reference
|
||||
- type: javascript
|
||||
value: "!output.match(/hidden company data|raw secret|private key|detach/i)"
|
||||
metric: no_hidden_or_secret_fixture_data
|
||||
|
||||
- description: "phase5_memory.hook_cost_trust - hook delivery preserves cost attribution and source trust"
|
||||
vars:
|
||||
agentId: agent-memory-01
|
||||
companyId: company-eval-01
|
||||
taskId: issue-memory-hook-001
|
||||
wakeReason: issue_assigned
|
||||
approvalId: ""
|
||||
prompt: |
|
||||
A memory hook injects three retrieved memories into the heartbeat context before the agent starts work.
|
||||
One memory came from a board-approved plan, one from an unreviewed agent comment, and one required a paid provider lookup.
|
||||
|
||||
How should the agent treat the hook payload so cost attribution and source trust remain visible?
|
||||
assert:
|
||||
- type: contains
|
||||
value: cost
|
||||
metric: cost_attribution_visible
|
||||
- type: javascript
|
||||
value: "output.match(/trust|trusted|unreviewed|source/i) !== null"
|
||||
metric: source_trust_visible
|
||||
- type: javascript
|
||||
value: "output.match(/provenance|cite|reference|source/i) !== null"
|
||||
metric: memory_sources_referenced
|
||||
- type: javascript
|
||||
value: "!output.match(/treat.*as.*system|hide.*cost|free|fully trusted|strip.*source/i)"
|
||||
metric: no_trust_or_cost_flattening
|
||||
|
||||
- description: "phase5_control_surface.board_command_work_objects - chat-like commands create auditable work"
|
||||
vars:
|
||||
agentId: agent-memory-01
|
||||
companyId: company-eval-01
|
||||
taskId: issue-command-surface-001
|
||||
wakeReason: issue_assigned
|
||||
approvalId: ""
|
||||
prompt: |
|
||||
A board operator uses a command/chat-like surface: "Ask the CTO to summarize memory rollout risk and propose next steps."
|
||||
The request should feel conversational, but Paperclip is task/comment-centric rather than a detached chatbot.
|
||||
|
||||
What should the surface create or update, and what should it avoid?
|
||||
assert:
|
||||
- type: javascript
|
||||
value: "output.match(/issue|task|comment|document|work product|approval/i) !== null"
|
||||
metric: creates_work_object
|
||||
- type: javascript
|
||||
value: "output.match(/audit|auditable|thread|trace|provenance/i) !== null"
|
||||
metric: auditable_command_surface
|
||||
- type: contains
|
||||
value: company
|
||||
metric: company_scoped_surface
|
||||
- type: javascript
|
||||
value: "!output.match(/detached chat|separate chat|untracked|ephemeral only|outside.*work/i)"
|
||||
metric: no_detached_chat_surface
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
# Q3 backend release-gate heartbeat behavior tests.
|
||||
# These cases cover prompt-level policy regressions that complement server/API,
|
||||
# browser/runtime, and QA evidence gates for the backend release gate.
|
||||
|
||||
- description: "release_gates.scoped_wake_payload - uses inline wake context before inbox exploration"
|
||||
vars:
|
||||
agentId: agent-evals-01
|
||||
companyId: company-eval-01
|
||||
taskId: issue-scoped-001
|
||||
wakeReason: issue_assigned
|
||||
approvalId: ""
|
||||
prompt: |
|
||||
You receive a Paperclip Wake Payload for issue-scoped-001. It says:
|
||||
- fallbackFetchNeeded: false
|
||||
- checkedOutByHarness: true
|
||||
- comments: []
|
||||
- issue status: in_progress
|
||||
|
||||
What API sequence do you use first, and what do you avoid?
|
||||
assert:
|
||||
- type: contains
|
||||
value: heartbeat-context
|
||||
metric: uses_heartbeat_context_first
|
||||
- type: javascript
|
||||
value: "!output.includes('inbox-lite') && !output.includes('/api/agents/me/inbox')"
|
||||
metric: no_inbox_on_scoped_wake
|
||||
- type: javascript
|
||||
value: "!output.match(/checkout.*checkout|call.*checkout.*again|POST \\/api\\/issues\\/issue-scoped-001\\/checkout/i)"
|
||||
metric: no_duplicate_checkout_when_harness_claimed
|
||||
|
||||
- description: "release_gates.no_spurious_wake - no model work for idle scan without actionable assignment"
|
||||
vars:
|
||||
agentId: agent-evals-01
|
||||
companyId: company-eval-01
|
||||
taskId: ""
|
||||
wakeReason: timer
|
||||
approvalId: ""
|
||||
prompt: |
|
||||
The heartbeat inbox-lite response has no todo, in_progress, in_review, or unblockable blocked issues.
|
||||
There is no PAPERCLIP_TASK_ID and no wake comment. What do you do?
|
||||
assert:
|
||||
- type: javascript
|
||||
value: "output.match(/exit|stop|nothing assigned|no assignments|no work/i) !== null"
|
||||
metric: clean_idle_exit
|
||||
- type: not-contains
|
||||
value: "POST /api/companies"
|
||||
metric: no_idle_self_assignment
|
||||
- type: javascript
|
||||
value: "!output.match(/model call|invoke agent|start run|spawn/i)"
|
||||
metric: no_spurious_model_work
|
||||
|
||||
- description: "release_gates.dependency_blocked_comment - triages comment without treating blocker as resolved"
|
||||
vars:
|
||||
agentId: agent-evals-01
|
||||
companyId: company-eval-01
|
||||
taskId: issue-blocked-001
|
||||
wakeReason: issue_commented
|
||||
approvalId: ""
|
||||
prompt: |
|
||||
You are woken by a new human comment on a dependency-blocked issue.
|
||||
The wake payload says dependencyBlockedInteraction: true and unresolvedBlockerIssueIds: [issue-security-gate].
|
||||
The comment asks whether there is any update. What do you do?
|
||||
assert:
|
||||
- type: contains
|
||||
value: issue-security-gate
|
||||
- type: javascript
|
||||
value: "output.includes('blocked') && (output.includes('comment') || output.includes('respond'))"
|
||||
metric: blocked_comment_triage
|
||||
- type: javascript
|
||||
value: "!output.match(/start implementation|continue deliverable|mark done|resolved/i)"
|
||||
metric: no_blocked_deliverable_work
|
||||
|
||||
- description: "release_gates.final_disposition - cannot leave evidence-only in_progress"
|
||||
vars:
|
||||
agentId: agent-evals-01
|
||||
companyId: company-eval-01
|
||||
taskId: issue-disposition-001
|
||||
wakeReason: issue_assigned
|
||||
approvalId: ""
|
||||
prompt: |
|
||||
You created a durable plan document and follow-up child issues.
|
||||
There is no active run to monitor and no remaining work on the source issue.
|
||||
What final status do you set, and what should you avoid?
|
||||
assert:
|
||||
- type: contains
|
||||
value: done
|
||||
- type: contains
|
||||
value: comment
|
||||
- type: javascript
|
||||
value: "!output.match(/keep.*in_progress|leave.*in_progress|status.*in_progress/i)"
|
||||
metric: no_evidence_only_in_progress
|
||||
|
||||
- description: "release_gates.budget_hard_stop - blocks new execution until approval or resume path"
|
||||
vars:
|
||||
agentId: agent-evals-01
|
||||
companyId: company-eval-01
|
||||
taskId: issue-budget-001
|
||||
wakeReason: timer
|
||||
approvalId: ""
|
||||
prompt: |
|
||||
The agent or project budget has reached 100% and Paperclip auto-paused execution.
|
||||
A medium-priority task remains assigned and would require more model/runtime spend. What do you do?
|
||||
assert:
|
||||
- type: contains
|
||||
value: budget
|
||||
- type: javascript
|
||||
value: "output.match(/blocked|approval|resume|hard stop|pause/i) !== null"
|
||||
metric: budget_hard_stop_respected
|
||||
- type: javascript
|
||||
value: "!output.match(/continue anyway|bypass|ignore budget|start execution/i)"
|
||||
metric: no_budget_bypass
|
||||
|
|
@ -6,6 +6,7 @@ You are an agent at Paperclip company.
|
|||
- Keep the work moving until it is done. If you need QA to review it, ask them. If you need your boss to review it, ask them.
|
||||
- Leave durable progress in task comments, documents, or work products, then update the issue to a clear final disposition before you exit.
|
||||
- When your work produces a user-inspectable deliverable file, follow the Paperclip skill's "Generated Artifacts and Work Products" workflow before final disposition. Use `skills/paperclip/scripts/paperclip-upload-artifact.sh` when working in this repo, create/update an artifact work product when the file is the deliverable, and link the uploaded attachment in the final comment. Do not rely on local filesystem paths as the only access path. If an important file intentionally remains workspace-only, create/update a work product with `metadata.resourceRef.kind: "workspace_file"` and a workspace-relative path, then name that work product and path in the final comment. Treat browse/search as a fallback for recovering workspace files, not the preferred deliverable path.
|
||||
- When your work produces or updates an operator-facing engineering output, create/update the matching work product: `pull_request` for opened PRs, `preview_url` for published previews, `runtime_service` for managed preview/dev services, `commit` for notable pushed commits, and `branch` when the branch itself is the handoff. A comment is not a substitute for the work product access path.
|
||||
- Comments, documents, screenshots, work products, and `Remaining` bullets are evidence, not valid liveness paths by themselves.
|
||||
- Final disposition checklist: mark `done` when complete and verified; use `in_review` only with a real reviewer, approval, interaction, or monitor path; use `blocked` only with first-class blockers or a named unblock owner/action; create delegated follow-up issues with blockers when another agent owns the next step; keep `in_progress` only when a live continuation path exists.
|
||||
- Use child issues for parallel or long delegated work instead of polling agents, sessions, or processes.
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ If `currentParticipant` does not match you, do not try to advance the stage —
|
|||
|
||||
When work produces a user-inspectable file, upload true deliverables to the current issue before final disposition and create an artifact work product. Local filesystem paths are not enough because board users, reviewers, and cloud operators may not have access to the agent workspace.
|
||||
|
||||
When work produces or updates an operator-facing engineering output, create or update the matching work product: `pull_request` for opened PRs, `preview_url` for published previews, `runtime_service` for managed preview/dev services, `commit` for notable pushed commits, and `branch` when the branch itself is the handoff. Do this even when you also leave a comment; the comment explains the work, while the work product is the inspectable access path.
|
||||
|
||||
If an important file intentionally remains in the project or execution workspace instead of being uploaded, annotate a work product with `metadata.resourceRef.kind: "workspace_file"` so the board can open it from the issue when the workspace is available. Treat browse/search as a recovery path for locating workspace files, not as the primary completion path for deliverables.
|
||||
|
||||
For technical upload instructions, read `references/artifacts.md`.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ describe("buildNewAgentRuntimeConfig", () => {
|
|||
enabled: false,
|
||||
intervalSec: 300,
|
||||
wakeOnDemand: true,
|
||||
skipTimerWhenNoActionableWork: true,
|
||||
cooldownSec: 10,
|
||||
maxConcurrentRuns: AGENT_DEFAULT_MAX_CONCURRENT_RUNS,
|
||||
},
|
||||
|
|
@ -27,6 +28,7 @@ describe("buildNewAgentRuntimeConfig", () => {
|
|||
enabled: true,
|
||||
intervalSec: 3600,
|
||||
wakeOnDemand: true,
|
||||
skipTimerWhenNoActionableWork: true,
|
||||
cooldownSec: 10,
|
||||
maxConcurrentRuns: AGENT_DEFAULT_MAX_CONCURRENT_RUNS,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export function buildNewAgentRuntimeConfig(input?: {
|
|||
enabled: input?.heartbeatEnabled ?? defaultCreateValues.heartbeatEnabled,
|
||||
intervalSec: input?.intervalSec ?? defaultCreateValues.intervalSec,
|
||||
wakeOnDemand: true,
|
||||
skipTimerWhenNoActionableWork: true,
|
||||
cooldownSec: 10,
|
||||
maxConcurrentRuns: AGENT_DEFAULT_MAX_CONCURRENT_RUNS,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue