recursive-agent 0.6.0

A minimal, orthogonal, self-improving coding agent kernel in Rust
Documentation
name: "HTTP API Server (AgentRuntime, port 9090)"
description: |
  Verify the HTTP server (Goal I) runs correctly with AgentRuntime: health,
  tools/openapi discovery, stateless /run, error envelopes, full session
  lifecycle (create → message → multi-turn → detail → list → delete), SSE
  streaming, and metrics.

  Server lifecycle is owned by setup/teardown. The session id is shared
  across cases via /tmp/http-sid (a fixture file), since ArgusAI's `save:`
  cannot capture exec stdout (engine limitation, see 11-session-resume).
  SSE assertion uses curl/grep — keeping the in-container network path
  rather than migrating to native sse: avoids a behavioral change.

  Rate limiting is exercised in 08b-http-rate-limit.yaml.
sequential: true

setup:
  - name: "Kill any leftover server on port 9090"
    ignoreError: true
    exec:
      container: recursive-e2e
      command: |
        pkill -f 'http --addr 0.0.0.0:9090' 2>/dev/null || true
        sleep 1
        echo cleaned

  - name: "Reset state files (sid, sse-sid, sse-events, sess.json) + workspace artifacts"
    ignoreError: true
    exec:
      container: recursive-e2e
      command: |
        rm -f /tmp/http-sid /tmp/sse-sid /tmp/sse-events.txt /tmp/sess.json /tmp/http-9090.log
        rm -f /workspace/http-run.txt /workspace/session-turn1.txt /workspace/session-turn2.txt /workspace/sse-test.txt
        echo reset

  - name: "Start HTTP server in background on 9090"
    exec:
      container: recursive-e2e
      command: |
        RECURSIVE_RATE_LIMIT_BURST=200 \
        RECURSIVE_RATE_LIMIT_RPM=6000 \
        nohup recursive \
          --api-base http://aimock:4010/v1 \
          --api-key mock-key -m mock-chat \
          http --addr 0.0.0.0:9090 \
          > /tmp/http-9090.log 2>&1 &
        sleep 2
        echo started

  - name: "Sanity: GET /health returns ok before running cases"
    exec:
      container: recursive-e2e
      command: curl -sf http://localhost:9090/health
    expect:
      exitCode: 0
      output:
        contains: "ok"

cases:
  # ── Discovery endpoints ──
  - name: "GET /tools returns read_file in JSON array"
    exec:
      container: recursive-e2e
      command: curl -sf http://localhost:9090/tools
    expect:
      exitCode: 0
      output:
        contains: "read_file"

  - name: "GET /openapi.json returns OpenAPI spec"
    exec:
      container: recursive-e2e
      command: curl -sf http://localhost:9090/openapi.json
    expect:
      exitCode: 0
      output:
        contains: "Recursive Agent API"

  # ── Error envelopes for non-existent session ──
  - name: "GET /sessions/:id returns 404 for non-existent session"
    exec:
      container: recursive-e2e
      command: >
        curl -s -o /dev/null -w 'status=%{http_code}'
        http://localhost:9090/sessions/nonexistent-session-id-xyz
    expect:
      exitCode: 0
      output:
        contains: "status=404"

  - name: "POST /sessions/:id/messages returns 404 for non-existent session"
    exec:
      container: recursive-e2e
      command: >
        curl -s -o /dev/null -w 'status=%{http_code}'
        -X POST http://localhost:9090/sessions/nonexistent-session-id-xyz/messages
        -H 'Content-Type: application/json'
        -d '{"content":"hello"}'
    expect:
      exitCode: 0
      output:
        contains: "status=404"

  - name: "GET /sessions/:id/events returns 404 for non-existent session"
    exec:
      container: recursive-e2e
      command: >
        curl -s -o /dev/null -w 'status=%{http_code}'
        http://localhost:9090/sessions/nonexistent-session-id-xyz/events
    expect:
      exitCode: 0
      output:
        contains: "status=404"

  # ── Stateless /run (ephemeral AgentRuntime) ──
  - name: "POST /run (act + assert): finish_reason + messages + total_steps"
    exec:
      container: recursive-e2e
      command: >
        curl -sf -X POST http://localhost:9090/run
        -H 'Content-Type: application/json'
        -d '{"goal":"Write file http-run.txt with content run-ok","max_steps":5}'
    expect:
      exitCode: 0
      output:
        contains:
          - "finish_reason"
          - "messages"
          - "total_steps"

  - name: "POST /run actually wrote http-run.txt to workspace (defends against tool-not-found regression)"
    file:
      container: recursive-e2e
      path: /workspace/http-run.txt
      exists: true
      contains: "run-ok"

  - name: "POST /run with empty goal returns 400"
    exec:
      container: recursive-e2e
      command: >
        curl -s -o /dev/null -w status=%{http_code}
        -X POST http://localhost:9090/run
        -H 'Content-Type: application/json'
        -d '{"goal":""}'
    expect:
      exitCode: 0
      output:
        contains: "status=400"

  # ── Persistent session lifecycle (sid shared via /tmp/http-sid) ──
  - name: "POST /sessions (act): create session, save id to /tmp/http-sid"
    exec:
      container: recursive-e2e
      command: >
        curl -sf -X POST http://localhost:9090/sessions
        -H 'Content-Type: application/json'
        -d '{"system_prompt":"You are a test assistant."}'
        | tee /tmp/sess.json | jq -r .id > /tmp/http-sid
        && echo created=$(cat /tmp/http-sid)
    expect:
      exitCode: 0
      output:
        contains: "created="

  - name: "POST /sessions/:id/messages (act): execute first turn"
    exec:
      container: recursive-e2e
      command: >
        SID=$(cat /tmp/http-sid)
        && curl -sf -X POST http://localhost:9090/sessions/$SID/messages
        -H 'Content-Type: application/json'
        -d '{"content":"Write file session-turn1.txt with content turn1"}'
        | jq -r .role | xargs -I {} echo role={}
    expect:
      exitCode: 0
      output:
        contains: "role=assistant"

  - name: "Session turn 1 actually wrote session-turn1.txt"
    file:
      container: recursive-e2e
      path: /workspace/session-turn1.txt
      exists: true
      contains: "turn1"

  - name: "GET /sessions/:id returns transcript with messages"
    exec:
      container: recursive-e2e
      command: >
        SID=$(cat /tmp/http-sid)
        && curl -sf http://localhost:9090/sessions/$SID
        | jq '.messages | length' | xargs -I {} echo count={}
    expect:
      exitCode: 0
      output:
        contains: "count="

  - name: "POST /sessions/:id/messages (act 2): second turn keeps prior context"
    exec:
      container: recursive-e2e
      command: >
        SID=$(cat /tmp/http-sid)
        && curl -sf -X POST http://localhost:9090/sessions/$SID/messages
        -H 'Content-Type: application/json'
        -d '{"content":"Write file session-turn2.txt with content turn2"}'
        | jq -r .role | xargs -I {} echo role={}
    expect:
      exitCode: 0
      output:
        contains: "role=assistant"

  - name: "Session turn 2 actually wrote session-turn2.txt (cross-turn tool-call regression check)"
    file:
      container: recursive-e2e
      path: /workspace/session-turn2.txt
      exists: true
      contains: "turn2"

  # ── SSE streaming (in-container curl, kept conservative) ──
  - name: "POST /sessions (act): create dedicated SSE session"
    exec:
      container: recursive-e2e
      command: >
        curl -sf -X POST http://localhost:9090/sessions
        -H 'Content-Type: application/json'
        -d '{"system_prompt":"You are a test assistant."}'
        | jq -r .id > /tmp/sse-sid
        && echo "created=$(cat /tmp/sse-sid)"
    expect:
      exitCode: 0
      output:
        contains: "created="

  - name: "GET /sessions/:id/events delivers `done` when a run completes"
    exec:
      container: recursive-e2e
      command: |
        SSE_SID=$(cat /tmp/sse-sid)
        : > /tmp/sse-events.txt
        curl -s -N "http://localhost:9090/sessions/$SSE_SID/events" \
          > /tmp/sse-events.txt 2>/dev/null &
        SSE_PID=$!
        sleep 0.5
        curl -sf -X POST "http://localhost:9090/sessions/$SSE_SID/messages" \
          -H 'Content-Type: application/json' \
          -d '{"content":"Write file sse-test.txt with content sse-ok"}' \
          > /dev/null
        sleep 0.5
        kill $SSE_PID 2>/dev/null || true
        wait $SSE_PID 2>/dev/null || true
        grep -q "done" /tmp/sse-events.txt && echo "sse-received" || echo "sse-missing"
    expect:
      exitCode: 0
      output:
        contains: "sse-received"

  - name: "SSE turn actually wrote sse-test.txt to workspace"
    file:
      container: recursive-e2e
      path: /workspace/sse-test.txt
      exists: true
      contains: "sse-ok"

  - name: "DELETE /sessions/:id removes the SSE session"
    exec:
      container: recursive-e2e
      command: >
        SSE_SID=$(cat /tmp/sse-sid)
        && curl -sf -X DELETE http://localhost:9090/sessions/$SSE_SID
        && echo cleaned
    expect:
      exitCode: 0
      output:
        contains: "cleaned"

  # ── Listing & deletion of the main session ──
  - name: "GET /sessions lists sessions with message_count"
    exec:
      container: recursive-e2e
      command: curl -sf http://localhost:9090/sessions
    expect:
      exitCode: 0
      output:
        contains: "message_count"

  - name: "DELETE /sessions/:id removes session; subsequent GET returns 404"
    exec:
      container: recursive-e2e
      command: >
        SID=$(cat /tmp/http-sid)
        && curl -sf -X DELETE http://localhost:9090/sessions/$SID
        && CODE=$(curl -s -o /dev/null -w %{http_code}
        http://localhost:9090/sessions/$SID)
        && echo after_delete=$CODE
    expect:
      exitCode: 0
      output:
        contains: "after_delete=404"

  - name: "GET /metrics returns Prometheus counters"
    exec:
      container: recursive-e2e
      command: curl -sf http://localhost:9090/metrics
    expect:
      exitCode: 0
      output:
        contains:
          - "recursive_requests_total"
          - "recursive_agent_runs_total"
          - "recursive_agent_steps_total"

teardown:
  - name: "Stop HTTP server on 9090"
    ignoreError: true
    exec:
      container: recursive-e2e
      command: |
        pkill -f 'http --addr 0.0.0.0:9090' 2>/dev/null || true
        echo stopped

  - name: "Remove state fixtures and workspace artifacts"
    ignoreError: true
    exec:
      container: recursive-e2e
      command: |
        rm -f /tmp/http-sid /tmp/sse-sid /tmp/sse-events.txt /tmp/sess.json /tmp/http-9090.log
        rm -f /workspace/http-run.txt /workspace/session-turn1.txt /workspace/session-turn2.txt /workspace/sse-test.txt