Build CoreOrdered learning track

Learn Ai Coding Agent Part 014 Api Design Openapi First Agent Platform

11 min read2192 words
PrevNext
Lesson 1464 lesson track13–35 Build Core

title: Learn AI Coding Agent From Scratch - Part 014 description: API design OpenAPI-first untuk Honk-like AI coding agent platform: resource model, endpoint, schema, idempotency, error model, auth scope, worker API, artifact API, event API, dan contract-first workflow. series: learn-ai-coding-agent seriesTitle: Learn AI Coding Agent From Scratch order: 14 partTitle: API Design: OpenAPI-first Contract untuk Agent Platform tags:

  • ai-coding-agent
  • openapi
  • api-design
  • contract-first
  • rest-api
  • idempotency
  • problem-details
  • control-plane
  • worker-api date: 2026-07-03

Part 014 — API Design: OpenAPI-first Contract untuk Agent Platform

Part sebelumnya membuat state machine. Sekarang kita membuat pintu resmi untuk menggerakkan state machine itu.

Dalam Honk-like AI coding agent, API bukan sekadar CRUD:

POST /tasks
GET /tasks/{id}

API adalah boundary antara:

  • user dan task intake;
  • UI dan timeline;
  • CLI dan platform;
  • scheduler dan worker;
  • worker dan control plane;
  • verifier dan run lifecycle;
  • judge dan approval;
  • artifact storage dan reviewer;
  • git provider dan PR orchestration;
  • policy/admin dan governance.

Kalau API kabur, worker akan mengambil shortcut. Kalau worker mengambil shortcut, state machine akan dilanggar. Kalau state machine dilanggar, agent tidak bisa diaudit.

Karena itu kita memakai pendekatan OpenAPI-first.

OpenAPI-first berarti kontrak API ditulis dan dipikirkan sebelum controller, service, database, atau UI dibuat. OpenAPI Specification mendefinisikan interface HTTP yang language-agnostic sehingga manusia dan mesin bisa memahami kapabilitas service tanpa membaca source code atau sniffing traffic. OpenAPI 3.1.1 adalah versi yang direkomendasikan untuk proyek baru pada saat rilis patch 2024.


1. Apa yang dimaksud API-first di seri ini?

API-first bukan berarti kita menulis YAML besar lalu berhenti.

API-first berarti:

contract -> generated types -> server stubs -> client SDK -> contract tests -> implementation

Tujuannya:

  • boundary eksplisit;
  • resource naming stabil;
  • schema tidak liar;
  • error model konsisten;
  • client bisa dibuat otomatis;
  • worker tidak bergantung pada detail database;
  • UI bisa dibangun parallel;
  • compatibility bisa diuji;
  • dokumentasi tidak tertinggal dari implementasi.

Dalam agent platform, kontrak API juga menjadi governance layer. API menentukan siapa boleh submit task, siapa boleh approve, siapa boleh cancel, worker mana boleh report event, dan artifact mana boleh dibaca.


2. API surface utama

Kita akan membagi API menjadi beberapa kelompok.

Public Control Plane API
Internal Worker API
Artifact API
Approval API
Policy API
Webhook/Event API
Admin API

Tidak semua endpoint harus public.

2.1 Public Control Plane API

Dipakai oleh UI, CLI, internal portal, Slack bot, atau automation.

Contoh resource:

/sessions
/tasks
/runs
/artifacts
/pull-requests

2.2 Internal Worker API

Dipakai worker untuk mengambil lease, mengirim heartbeat, mencatat step, mengirim event, dan upload artifact.

Contoh:

/worker/runs:lease
/worker/runs/{runId}:heartbeat
/worker/runs/{runId}/events
/worker/runs/{runId}/steps

2.3 Artifact API

Dipakai untuk diff, log, verification report, judge report, plan, repository map, dan PR body draft.

Contoh:

/artifacts/{artifactId}
/artifacts/{artifactId}/content
/runs/{runId}/artifacts

2.4 Approval API

Dipakai ketika state machine berhenti di approval gate.

Contoh:

/approval-requests
/approval-requests/{id}:approve
/approval-requests/{id}:reject
/approval-requests/{id}:request-changes

2.5 Policy API

Dipakai admin/platform untuk mengelola capability, risk, allowed repo, forbidden path, budget, dan auto-PR policy.

Contoh:

/policies
/policy-evaluations

2.6 Webhook/Event API

Dipakai untuk menerima callback dari Git provider atau mengirim event keluar.

Contoh:

/webhooks/github
/events/subscriptions

3. Resource model

API harus mengikuti domain model yang sudah kita buat.

Resource utama:

ResourceMeaning
SessionKonteks interaksi atau batch request
TaskPermintaan kerja yang dinormalisasi
RunEksekusi konkret atas task
StepAksi granular selama run
ArtifactOutput immutable: diff, log, report, plan
PatchCandidate code change
VerificationReportHasil verifier
JudgeReportHasil judge
ApprovalRequestGate keputusan manusia
PullRequestRecordMetadata PR yang dibuat agent
PolicyEvaluationHasil evaluasi policy

4. Naming convention endpoint

Gunakan resource nouns untuk operasi biasa:

POST /tasks
GET /tasks/{taskId}
GET /tasks/{taskId}/runs
GET /runs/{runId}
GET /runs/{runId}/steps
GET /runs/{runId}/artifacts

Gunakan action suffix untuk command yang jelas bukan CRUD:

POST /tasks/{taskId}:cancel
POST /runs/{runId}:cancel
POST /approval-requests/{approvalRequestId}:approve
POST /worker/runs:lease
POST /worker/runs/{runId}:heartbeat

Kenapa bukan PATCH /runs/{id} untuk cancel?

Karena cancel adalah command dengan invariant dan side effect:

  • validasi state;
  • tulis event;
  • signal worker;
  • cleanup;
  • audit;
  • notifikasi.

Command endpoint membuat maksud lebih jelas.


5. Versioning

Untuk awal:

/api/v1

Contoh:

POST /api/v1/tasks
GET /api/v1/runs/{runId}

Jangan version per endpoint terlalu dini. Tetapi rancang schema supaya evolvable:

  • field baru additive;
  • enum value baru harus dipertimbangkan client;
  • deprecated field diberi lifecycle;
  • breaking change hanya di major API version;
  • response harus punya schemaVersion untuk artifact kompleks.

6. Authn dan authz scope

Agent platform berbahaya karena bisa membaca repo, menjalankan command, dan membuat PR. Maka API scope harus eksplisit.

Contoh scope:

ScopeCapability
agent.task.submitSubmit task
agent.task.readRead task
agent.task.cancelCancel task
agent.run.readRead run
agent.run.cancelCancel run
agent.artifact.readRead artifact metadata/content
agent.approval.decideApprove/reject/request changes
agent.policy.readRead policy
agent.policy.writeModify policy
agent.worker.leaseAcquire worker lease
agent.worker.reportReport worker events
agent.adminAdmin operations

Worker token tidak boleh punya scope user.

User token tidak boleh punya scope worker.


7. Idempotency key

Endpoint command harus mendukung idempotency.

Terutama:

POST /tasks
POST /worker/runs/{runId}/events
POST /approval-requests/{id}:approve
POST /pull-requests

Header:

Idempotency-Key: 01JZ4M9N5ZK4G2F3WQ6VR8S9A0

Response untuk retry dengan key sama harus sama secara semantik.

Idempotency penting karena:

  • client network retry;
  • worker crash setelah request berhasil;
  • PR creation tidak boleh dobel;
  • event transition tidak boleh dobel;
  • billing/cost tidak boleh dihitung ganda.

8. Correlation ID dan trace ID

Setiap response harus membawa correlation id.

X-Request-Id: req_01J...
X-Trace-Id: trace_01J...

Setiap resource penting juga punya id stabil:

task_...
run_...
step_...
artifact_...
patch_...
vr_...
judge_...
approval_...
pr_...

Jangan memakai auto-increment integer untuk resource lintas sistem. ID harus aman diekspos di URL dan log.


9. Error model

Jangan mengembalikan error bebas seperti:

{ "error": "failed" }

Gunakan shape konsisten, terinspirasi problem details.

{
  "type": "https://agent.example.com/problems/illegal-transition",
  "title": "Illegal state transition",
  "status": 409,
  "detail": "Run run_123 is in VERIFYING and cannot accept event PR_CREATED.",
  "instance": "/api/v1/runs/run_123/events/evt_456",
  "code": "ILLEGAL_TRANSITION",
  "requestId": "req_01J...",
  "metadata": {
    "runId": "run_123",
    "currentStatus": "VERIFYING",
    "eventType": "PR_CREATED"
  }
}

Common error codes:

HTTPCodeMeaning
400INVALID_REQUESTSchema validasi gagal
401UNAUTHENTICATEDToken tidak valid
403FORBIDDENScope/policy tidak mengizinkan
404NOT_FOUNDResource tidak ada atau tidak terlihat
409ILLEGAL_TRANSITIONState transition tidak legal
409IDEMPOTENCY_CONFLICTKey sama dengan payload berbeda
422TASK_CONTRACT_INVALIDTask secara domain tidak valid
423RESOURCE_LOCKEDRun/approval sedang dikunci
429RATE_LIMITEDQuota/rate limit
500INTERNAL_ERRORError tak terduga
503SERVICE_UNAVAILABLEDependency/service unavailable

10. Pagination dan filtering

List endpoint harus punya pagination sejak awal.

GET /api/v1/tasks?status=RUNNING&limit=50&pageToken=...

Response:

{
  "items": [],
  "nextPageToken": "..."
}

Gunakan cursor/page token, bukan offset, untuk data yang aktif berubah.

Filtering umum:

status
repository
createdBy
createdAfter
createdBefore
riskLevel
completionMode

Sorting default:

createdAt desc

11. OpenAPI skeleton

Berikut skeleton awal openapi.yaml.

openapi: 3.1.1
info:
  title: AI Coding Agent Platform API
  version: 1.0.0
  description: >
    Control plane API for a Honk-like background AI coding agent platform.
servers:
  - url: https://agent.example.com/api/v1
security:
  - bearerAuth: []
tags:
  - name: Tasks
  - name: Runs
  - name: Worker
  - name: Artifacts
  - name: Approvals
  - name: Policies
paths: {}
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

Kita akan menambahkan paths dan schemas secara bertahap.


12. Schema: Task

Task adalah permintaan kerja yang sudah dinormalisasi.

components:
  schemas:
    TaskStatus:
      type: string
      enum:
        - DRAFT
        - SUBMITTED
        - VALIDATING
        - READY
        - RUNNING
        - WAITING_FOR_APPROVAL
        - COMPLETED
        - FAILED
        - REJECTED
        - CANCELLED

    RiskLevel:
      type: string
      enum:
        - LOW
        - MEDIUM
        - HIGH
        - CRITICAL

    CompletionMode:
      type: string
      enum:
        - ANALYSIS_REPORT_CREATED
        - PATCH_READY_FOR_REVIEW
        - PR_CREATED
        - PR_MERGED

    Task:
      type: object
      required:
        - id
        - status
        - repository
        - instruction
        - riskLevel
        - completionMode
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          examples: [task_01JZ4N3YJ4T9K7C9S4YFXP4C5R]
        status:
          $ref: '#/components/schemas/TaskStatus'
        repository:
          $ref: '#/components/schemas/RepositoryRef'
        instruction:
          type: string
        riskLevel:
          $ref: '#/components/schemas/RiskLevel'
        completionMode:
          $ref: '#/components/schemas/CompletionMode'
        createdBy:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

Repository ref:

    RepositoryRef:
      type: object
      required:
        - provider
        - owner
        - name
        - defaultBranch
      properties:
        provider:
          type: string
          enum: [GITHUB, GITLAB, BITBUCKET, LOCAL]
        owner:
          type: string
        name:
          type: string
        defaultBranch:
          type: string
        targetBranch:
          type: string
        commitSha:
          type: string

13. Endpoint: submit task

paths:
  /tasks:
    post:
      tags: [Tasks]
      operationId: createTask
      summary: Submit a coding-agent task
      security:
        - bearerAuth: [agent.task.submit]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTaskRequest'
      responses:
        '202':
          description: Task accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateTaskResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'

Request:

components:
  schemas:
    CreateTaskRequest:
      type: object
      required:
        - repository
        - instruction
        - completionMode
      properties:
        repository:
          $ref: '#/components/schemas/RepositoryRef'
        instruction:
          type: string
          minLength: 20
          maxLength: 20000
        completionMode:
          $ref: '#/components/schemas/CompletionMode'
        riskHint:
          $ref: '#/components/schemas/RiskLevel'
        constraints:
          type: array
          items:
            type: string
        allowedPaths:
          type: array
          items:
            type: string
        forbiddenPaths:
          type: array
          items:
            type: string
        maxBudget:
          $ref: '#/components/schemas/BudgetLimit'

Response:

    CreateTaskResponse:
      type: object
      required:
        - task
      properties:
        task:
          $ref: '#/components/schemas/Task'
        firstRun:
          $ref: '#/components/schemas/RunSummary'

Kenapa response 202, bukan 201?

Karena task diterima untuk diproses asynchronous. Resource memang dibuat, tetapi pekerjaan belum selesai.


14. Endpoint: get task

  /tasks/{taskId}:
    get:
      tags: [Tasks]
      operationId: getTask
      summary: Get task details
      security:
        - bearerAuth: [agent.task.read]
      parameters:
        - $ref: '#/components/parameters/TaskId'
      responses:
        '200':
          description: Task details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '404':
          $ref: '#/components/responses/NotFound'

15. Endpoint: cancel task

  /tasks/{taskId}:cancel:
    post:
      tags: [Tasks]
      operationId: cancelTask
      summary: Cancel a task and active runs
      security:
        - bearerAuth: [agent.task.cancel]
      parameters:
        - $ref: '#/components/parameters/TaskId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CancelRequest'
      responses:
        '202':
          description: Cancellation accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '409':
          $ref: '#/components/responses/Conflict'

Cancel request:

    CancelRequest:
      type: object
      properties:
        reason:
          type: string
          maxLength: 1000

16. Schema: Run

components:
  schemas:
    RunStatus:
      type: string
      enum:
        - CREATED
        - QUEUED
        - LEASED
        - PREPARING
        - CONTEXT_BUILDING
        - PLANNING
        - EXECUTING
        - PATCH_READY
        - VERIFYING
        - JUDGING
        - WAITING_FOR_APPROVAL
        - PR_CREATING
        - PR_CREATED
        - SUCCEEDED
        - FAILED
        - CANCELLED
        - EXPIRED

    RunSummary:
      type: object
      required:
        - id
        - taskId
        - status
        - attemptNo
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
        taskId:
          type: string
        status:
          $ref: '#/components/schemas/RunStatus'
        attemptNo:
          type: integer
          minimum: 1
        currentStep:
          type: string
        failure:
          $ref: '#/components/schemas/FailureSummary'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    RunDetail:
      allOf:
        - $ref: '#/components/schemas/RunSummary'
        - type: object
          properties:
            timeline:
              type: array
              items:
                $ref: '#/components/schemas/RunTimelineEvent'
            latestPatch:
              $ref: '#/components/schemas/PatchSummary'
            latestVerificationReport:
              $ref: '#/components/schemas/VerificationReportSummary'
            latestJudgeReport:
              $ref: '#/components/schemas/JudgeReportSummary'
            pullRequest:
              $ref: '#/components/schemas/PullRequestSummary'

17. Endpoint: get run

  /runs/{runId}:
    get:
      tags: [Runs]
      operationId: getRun
      summary: Get run detail
      security:
        - bearerAuth: [agent.run.read]
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200':
          description: Run detail
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunDetail'
        '404':
          $ref: '#/components/responses/NotFound'

18. Endpoint: list run steps

  /runs/{runId}/steps:
    get:
      tags: [Runs]
      operationId: listRunSteps
      summary: List run steps
      security:
        - bearerAuth: [agent.run.read]
      parameters:
        - $ref: '#/components/parameters/RunId'
        - $ref: '#/components/parameters/PageToken'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: Run steps
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/Step'
                  nextPageToken:
                    type: string

Step schema:

    Step:
      type: object
      required:
        - id
        - runId
        - type
        - status
        - startedAt
      properties:
        id:
          type: string
        runId:
          type: string
        type:
          type: string
          enum:
            - PLAN
            - TOOL_CALL
            - FILE_EDIT
            - SHELL_COMMAND
            - VERIFICATION
            - JUDGE
            - PR_OPERATION
            - SYSTEM
        status:
          type: string
          enum: [CREATED, RUNNING, SUCCEEDED, FAILED, CANCELLED, TIMED_OUT]
        title:
          type: string
        summary:
          type: string
        startedAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
        artifactIds:
          type: array
          items:
            type: string

19. Worker API: lease run

Worker tidak mencari run langsung dari database. Worker meminta lease lewat API internal.

  /worker/runs:lease:
    post:
      tags: [Worker]
      operationId: leaseRun
      summary: Lease the next queued run for a worker
      security:
        - bearerAuth: [agent.worker.lease]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LeaseRunRequest'
      responses:
        '200':
          description: A run was leased
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LeaseRunResponse'
        '204':
          description: No run available

Request:

    LeaseRunRequest:
      type: object
      required:
        - workerId
        - capabilities
      properties:
        workerId:
          type: string
        capabilities:
          type: array
          items:
            type: string
        maxRiskLevel:
          $ref: '#/components/schemas/RiskLevel'
        supportedLanguages:
          type: array
          items:
            type: string
        supportedProviders:
          type: array
          items:
            type: string
            enum: [GITHUB, GITLAB, BITBUCKET, LOCAL]

Response:

    LeaseRunResponse:
      type: object
      required:
        - run
        - lease
      properties:
        run:
          $ref: '#/components/schemas/RunDetail'
        task:
          $ref: '#/components/schemas/Task'
        lease:
          $ref: '#/components/schemas/RunLease'

    RunLease:
      type: object
      required:
        - leaseToken
        - expiresAt
      properties:
        leaseToken:
          type: string
        expiresAt:
          type: string
          format: date-time
        heartbeatIntervalSeconds:
          type: integer

20. Worker API: heartbeat

  /worker/runs/{runId}:heartbeat:
    post:
      tags: [Worker]
      operationId: heartbeatRun
      summary: Renew worker lease heartbeat
      security:
        - bearerAuth: [agent.worker.report]
      parameters:
        - $ref: '#/components/parameters/RunId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HeartbeatRequest'
      responses:
        '204':
          description: Heartbeat accepted
        '409':
          $ref: '#/components/responses/Conflict'
    HeartbeatRequest:
      type: object
      required:
        - workerId
        - leaseToken
      properties:
        workerId:
          type: string
        leaseToken:
          type: string
        currentStepId:
          type: string

21. Worker API: report run event

Worker reports event. It does not set status directly.

  /worker/runs/{runId}/events:
    post:
      tags: [Worker]
      operationId: reportRunEvent
      summary: Report a run lifecycle event
      security:
        - bearerAuth: [agent.worker.report]
      parameters:
        - $ref: '#/components/parameters/RunId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReportRunEventRequest'
      responses:
        '202':
          description: Event accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunTransitionResponse'
        '409':
          $ref: '#/components/responses/Conflict'
    ReportRunEventRequest:
      type: object
      required:
        - workerId
        - leaseToken
        - eventType
      properties:
        workerId:
          type: string
        leaseToken:
          type: string
        eventType:
          type: string
          enum:
            - WORKER_STARTED
            - SANDBOX_READY
            - CONTEXT_READY
            - PLAN_CREATED
            - PATCH_CREATED
            - VERIFICATION_STARTED
            - VERIFICATION_FAILED_RETRYABLE
            - VERIFICATION_PASSED
            - JUDGE_REQUIRES_FIX
            - HUMAN_APPROVAL_REQUIRED
            - AUTO_PR_ALLOWED
            - PR_CREATED
            - COMPLETION_CONDITION_MET
            - NON_RETRYABLE_FAILURE
        artifactIds:
          type: array
          items:
            type: string
        payload:
          type: object
          additionalProperties: true

Response:

    RunTransitionResponse:
      type: object
      required:
        - runId
        - fromStatus
        - toStatus
      properties:
        runId:
          type: string
        fromStatus:
          $ref: '#/components/schemas/RunStatus'
        toStatus:
          $ref: '#/components/schemas/RunStatus'
        acceptedAt:
          type: string
          format: date-time

22. Artifact API

Artifact adalah immutable evidence. Jangan update artifact content setelah dibuat.

Artifact metadata:

    Artifact:
      type: object
      required:
        - id
        - runId
        - type
        - contentType
        - sizeBytes
        - createdAt
      properties:
        id:
          type: string
        runId:
          type: string
        stepId:
          type: string
        type:
          type: string
          enum:
            - PLAN
            - REPOSITORY_MAP
            - DIFF
            - PATCH
            - COMMAND_LOG
            - VERIFICATION_REPORT
            - JUDGE_REPORT
            - PR_BODY
            - SCREENSHOT
            - OTHER
        contentType:
          type: string
        sha256:
          type: string
        sizeBytes:
          type: integer
          minimum: 0
        redactionStatus:
          type: string
          enum: [NOT_SCANNED, CLEAN, REDACTED, BLOCKED]
        createdAt:
          type: string
          format: date-time

Endpoint:

  /runs/{runId}/artifacts:
    get:
      tags: [Artifacts]
      operationId: listRunArtifacts
      security:
        - bearerAuth: [agent.artifact.read]
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200':
          description: Artifact list
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/Artifact'

  /artifacts/{artifactId}:
    get:
      tags: [Artifacts]
      operationId: getArtifact
      security:
        - bearerAuth: [agent.artifact.read]
      parameters:
        - $ref: '#/components/parameters/ArtifactId'
      responses:
        '200':
          description: Artifact metadata
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Artifact'

  /artifacts/{artifactId}/content:
    get:
      tags: [Artifacts]
      operationId: downloadArtifactContent
      security:
        - bearerAuth: [agent.artifact.read]
      parameters:
        - $ref: '#/components/parameters/ArtifactId'
      responses:
        '200':
          description: Artifact content
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary

Artifact read harus menerapkan redaction policy. Jangan mengembalikan log mentah yang mungkin mengandung secret sebelum secret scan/redaction.


23. Approval API

Approval request dibuat ketika state machine masuk WAITING_FOR_APPROVAL.

Schema:

    ApprovalRequest:
      type: object
      required:
        - id
        - taskId
        - runId
        - status
        - reason
        - createdAt
      properties:
        id:
          type: string
        taskId:
          type: string
        runId:
          type: string
        status:
          type: string
          enum: [OPEN, APPROVED, REJECTED, CHANGES_REQUESTED, EXPIRED]
        reason:
          type: string
        requestedDecision:
          type: string
          enum: [APPROVE_PR_CREATION, APPROVE_PLAN, APPROVE_RETRY, APPROVE_HIGH_RISK_CHANGE]
        artifactIds:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        decidedAt:
          type: string
          format: date-time

Approve endpoint:

  /approval-requests/{approvalRequestId}:approve:
    post:
      tags: [Approvals]
      operationId: approveRequest
      security:
        - bearerAuth: [agent.approval.decide]
      parameters:
        - $ref: '#/components/parameters/ApprovalRequestId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApprovalDecisionRequest'
      responses:
        '200':
          description: Approval accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalRequest'

Request changes:

  /approval-requests/{approvalRequestId}:request-changes:
    post:
      tags: [Approvals]
      operationId: requestApprovalChanges
      security:
        - bearerAuth: [agent.approval.decide]
      parameters:
        - $ref: '#/components/parameters/ApprovalRequestId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RequestChangesRequest'
      responses:
        '200':
          description: Changes requested
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApprovalRequest'
    ApprovalDecisionRequest:
      type: object
      properties:
        comment:
          type: string
          maxLength: 4000

    RequestChangesRequest:
      type: object
      required:
        - instruction
      properties:
        instruction:
          type: string
          minLength: 5
          maxLength: 4000

24. Patch and verification schemas

Patch summary:

    PatchSummary:
      type: object
      required:
        - id
        - runId
        - changedFiles
        - additions
        - deletions
        - artifactId
      properties:
        id:
          type: string
        runId:
          type: string
        changedFiles:
          type: array
          items:
            type: string
        additions:
          type: integer
        deletions:
          type: integer
        artifactId:
          type: string
        summary:
          type: string

Verification report:

    VerificationReportSummary:
      type: object
      required:
        - id
        - runId
        - status
        - startedAt
        - completedAt
      properties:
        id:
          type: string
        runId:
          type: string
        status:
          type: string
          enum: [PASSED, FAILED, TIMED_OUT, CANCELLED]
        commands:
          type: array
          items:
            $ref: '#/components/schemas/VerificationCommandResult'
        summary:
          type: string
        startedAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time

    VerificationCommandResult:
      type: object
      required:
        - command
        - status
        - durationMs
      properties:
        command:
          type: string
        status:
          type: string
          enum: [PASSED, FAILED, TIMED_OUT, SKIPPED]
        exitCode:
          type: integer
        durationMs:
          type: integer
        logArtifactId:
          type: string

Judge report:

    JudgeReportSummary:
      type: object
      required:
        - id
        - runId
        - verdict
      properties:
        id:
          type: string
        runId:
          type: string
        verdict:
          type: string
          enum: [PASS, NEEDS_FIX, REJECT, NEEDS_HUMAN]
        confidence:
          type: number
          minimum: 0
          maximum: 1
        reasons:
          type: array
          items:
            type: string
        artifactId:
          type: string

25. Pull request record API

PR record bukan GitHub PR penuh. Ini metadata yang platform butuhkan.

    PullRequestSummary:
      type: object
      required:
        - id
        - provider
        - url
        - branch
        - status
      properties:
        id:
          type: string
        provider:
          type: string
          enum: [GITHUB, GITLAB, BITBUCKET]
        externalId:
          type: string
        url:
          type: string
          format: uri
        branch:
          type: string
        baseBranch:
          type: string
        commitSha:
          type: string
        status:
          type: string
          enum: [CREATING, OPEN, UPDATED, MERGED, CLOSED, FAILED]

Endpoint:

  /runs/{runId}/pull-request:
    get:
      tags: [Runs]
      operationId: getRunPullRequest
      security:
        - bearerAuth: [agent.run.read]
      parameters:
        - $ref: '#/components/parameters/RunId'
      responses:
        '200':
          description: Pull request record
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PullRequestSummary'
        '404':
          $ref: '#/components/responses/NotFound'

26. Policy evaluation API

Sebelum task jalan, policy harus bisa dievaluasi dan dijelaskan.

  /policy-evaluations:
    post:
      tags: [Policies]
      operationId: evaluatePolicy
      summary: Evaluate whether a task is allowed
      security:
        - bearerAuth: [agent.policy.read]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTaskRequest'
      responses:
        '200':
          description: Policy evaluation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyEvaluationResult'

Schema:

    PolicyEvaluationResult:
      type: object
      required:
        - decision
        - riskLevel
        - reasons
      properties:
        decision:
          type: string
          enum: [ALLOW, ALLOW_WITH_APPROVAL, DENY]
        riskLevel:
          $ref: '#/components/schemas/RiskLevel'
        reasons:
          type: array
          items:
            type: string
        requiredApprovals:
          type: array
          items:
            type: string
        effectiveLimits:
          $ref: '#/components/schemas/BudgetLimit'

27. Budget schema

Agent API harus membuat budget eksplisit.

    BudgetLimit:
      type: object
      properties:
        maxDurationSeconds:
          type: integer
          minimum: 1
        maxSteps:
          type: integer
          minimum: 1
        maxToolCalls:
          type: integer
          minimum: 1
        maxInputTokens:
          type: integer
          minimum: 1
        maxOutputTokens:
          type: integer
          minimum: 1
        maxCostUsd:
          type: number
          minimum: 0

Budget bukan optimisasi belakangan. Budget adalah safety control.


28. Common parameters

components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
        minLength: 8
        maxLength: 200

    TaskId:
      name: taskId
      in: path
      required: true
      schema:
        type: string

    RunId:
      name: runId
      in: path
      required: true
      schema:
        type: string

    ArtifactId:
      name: artifactId
      in: path
      required: true
      schema:
        type: string

    ApprovalRequestId:
      name: approvalRequestId
      in: path
      required: true
      schema:
        type: string

    PageToken:
      name: pageToken
      in: query
      required: false
      schema:
        type: string

    Limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50

29. Common responses

components:
  responses:
    BadRequest:
      description: Invalid request
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Forbidden:
      description: Forbidden
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    NotFound:
      description: Not found
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Conflict:
      description: Conflict
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    UnprocessableEntity:
      description: Domain validation failed
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'

Problem schema:

    Problem:
      type: object
      required:
        - type
        - title
        - status
        - code
        - requestId
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        instance:
          type: string
        code:
          type: string
        requestId:
          type: string
        metadata:
          type: object
          additionalProperties: true

30. Example: submit task with curl

curl -X POST "https://agent.example.com/api/v1/tasks" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: upgrade-service-a-20260703-001" \
  -d '{
    "repository": {
      "provider": "GITHUB",
      "owner": "example-org",
      "name": "service-a",
      "defaultBranch": "main"
    },
    "instruction": "Upgrade usage of deprecated FooClient v1 API to FooClient v2. Keep behavior compatible. Add or update tests if needed.",
    "completionMode": "PR_CREATED",
    "riskHint": "MEDIUM",
    "forbiddenPaths": ["infra/prod/**", "secrets/**"],
    "maxBudget": {
      "maxDurationSeconds": 1800,
      "maxSteps": 80,
      "maxCostUsd": 5.0
    }
  }'

Expected response:

{
  "task": {
    "id": "task_01JZ4N3YJ4T9K7C9S4YFXP4C5R",
    "status": "READY",
    "repository": {
      "provider": "GITHUB",
      "owner": "example-org",
      "name": "service-a",
      "defaultBranch": "main"
    },
    "instruction": "Upgrade usage of deprecated FooClient v1 API to FooClient v2. Keep behavior compatible. Add or update tests if needed.",
    "riskLevel": "MEDIUM",
    "completionMode": "PR_CREATED",
    "createdBy": "user_123",
    "createdAt": "2026-07-03T10:00:00Z",
    "updatedAt": "2026-07-03T10:00:01Z"
  },
  "firstRun": {
    "id": "run_01JZ4N4C1KS6FW9XMBYK5J1E3B",
    "taskId": "task_01JZ4N3YJ4T9K7C9S4YFXP4C5R",
    "status": "QUEUED",
    "attemptNo": 1,
    "createdAt": "2026-07-03T10:00:01Z",
    "updatedAt": "2026-07-03T10:00:01Z"
  }
}

31. Example: worker lease

curl -X POST "https://agent.example.com/api/v1/worker/runs:lease" \
  -H "Authorization: Bearer $WORKER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "workerId": "worker-java-001",
    "capabilities": ["java", "maven", "github", "container-sandbox"],
    "maxRiskLevel": "MEDIUM",
    "supportedLanguages": ["java", "typescript"],
    "supportedProviders": ["GITHUB"]
  }'

Expected response includes:

{
  "lease": {
    "leaseToken": "lease_01JZ4N7N4TJ64E8MJGQ7TA8Y1X",
    "expiresAt": "2026-07-03T10:05:00Z",
    "heartbeatIntervalSeconds": 15
  }
}

32. Example: report patch created

curl -X POST "https://agent.example.com/api/v1/worker/runs/run_01JZ4N4C1KS6FW9XMBYK5J1E3B/events" \
  -H "Authorization: Bearer $WORKER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: run_01JZ4N4C_patch_001" \
  -d '{
    "workerId": "worker-java-001",
    "leaseToken": "lease_01JZ4N7N4TJ64E8MJGQ7TA8Y1X",
    "eventType": "PATCH_CREATED",
    "artifactIds": ["artifact_diff_001"],
    "payload": {
      "changedFiles": [
        "src/main/java/com/example/FooAdapter.java",
        "src/test/java/com/example/FooAdapterTest.java"
      ],
      "additions": 84,
      "deletions": 42
    }
  }'

Expected response:

{
  "runId": "run_01JZ4N4C1KS6FW9XMBYK5J1E3B",
  "fromStatus": "EXECUTING",
  "toStatus": "PATCH_READY",
  "acceptedAt": "2026-07-03T10:18:22Z"
}

33. Contract tests

OpenAPI contract harus diuji.

Minimal test:

TestPurpose
request validationinvalid request ditolak
response validationserver tidak mengembalikan shape liar
enum compatibilitystatus baru tidak memecahkan client
idempotencyretry key sama tidak membuat resource ganda
auth scopetoken salah ditolak
illegal transitionevent salah menghasilkan 409
paginationlist endpoint stabil
problem responseerror shape konsisten

Contract test flow:

load openapi.yaml
start test server
send valid examples
send invalid examples
validate responses against spec
fail build if contract drift

34. Generated clients

Dari OpenAPI, kita bisa generate:

  • TypeScript client untuk UI;
  • Java client untuk worker;
  • Go client untuk CLI;
  • mock server untuk integration test;
  • schema validator untuk request/response;
  • API docs.

Tetapi jangan percaya generated code tanpa review. OpenAPI-first bukan berarti generated everything. Domain logic tetap handwritten.


35. API compatibility rules

Perubahan aman:

  • tambah optional response field;
  • tambah endpoint baru;
  • tambah optional request field dengan default;
  • tambah enum value jika client siap unknown value;
  • tambah error code baru yang masih sesuai kategori HTTP.

Perubahan berbahaya:

  • hapus field;
  • ubah required field;
  • ubah semantic enum;
  • ubah status code sukses;
  • ubah idempotency behavior;
  • ubah pagination contract;
  • ubah error shape.

Untuk enum, client harus defensif:

unknown RunStatus -> display as UNKNOWN and do not crash

36. Security considerations in API design

Agent API harus menghindari beberapa bahaya.

36.1 Secret in request body

Jangan izinkan user menyisipkan credentials mentah di task instruction.

Buruk:

{
  "instruction": "Use token ghp_xxx to clone repo..."
}

Lebih baik:

{
  "repositoryCredentialRef": "cred_github_installation_123"
}

36.2 Artifact content leak

Log command bisa mengandung secret. Artifact content endpoint harus melewati redaction policy.

36.3 Worker privilege escalation

Worker token harus scoped ke worker API. Worker tidak boleh bisa approve high-risk change.

36.4 Repo path traversal

File path dalam artifact atau patch metadata harus dinormalisasi. Jangan percaya path dari worker begitu saja.

36.5 Event forgery

Worker event harus membawa lease token valid. Kalau lease expired, event ditolak.


37. Why API should not expose raw prompts by default

Prompt bisa mengandung:

  • internal policy;
  • repository instructions;
  • summarized code context;
  • user instruction;
  • tool output;
  • sensitive error log;
  • security control text.

Jangan expose raw prompt di public API.

Lebih aman:

GET /runs/{runId}/steps -> summarized step timeline
GET /artifacts/{id}/content -> redacted artifact based on permission

Raw prompt boleh ada di audit store dengan akses terbatas.


38. API lifecycle diagram


39. Minimal file layout for contracts

Letakkan contract di tempat yang jelas:

contracts/
  openapi/
    agent-platform.v1.yaml
    components/
      task.yaml
      run.yaml
      artifact.yaml
      approval.yaml
      problem.yaml
  examples/
    create-task.request.json
    create-task.response.json
    run-detail.response.json
  tests/
    contract/
      create-task.test.ts
      run-transition.test.ts

Jangan biarkan OpenAPI tersimpan tersembunyi di controller annotation saja. Annotation boleh dipakai untuk membantu, tetapi source of truth sebaiknya file contract yang direview.


40. Implementation plan

Untuk part implementasi nanti, kita akan membangun API dengan urutan ini:

  1. agent-platform.v1.yaml minimal;
  2. schema Task, Run, Problem;
  3. endpoint POST /tasks;
  4. endpoint GET /runs/{runId};
  5. endpoint worker lease;
  6. endpoint worker event;
  7. idempotency middleware;
  8. problem response mapper;
  9. OpenAPI validation test;
  10. generated client untuk worker.

Kita belum masuk code penuh di part ini karena tujuan part ini adalah desain kontrak.


41. Anti-patterns API agent platform

41.1 Generic update endpoint

Buruk:

PATCH /runs/{id}
{ "status": "SUCCEEDED" }

Ini membypass state machine.

41.2 Satu endpoint super besar

Buruk:

POST /agent/do-everything

Sulit diaudit, sulit di-retry, sulit di-secure.

41.3 Worker membaca database langsung

Ini mencampur execution plane dan control plane.

41.4 Tidak ada idempotency

Akan membuat duplicate task, duplicate event, duplicate PR.

41.5 Error response tidak konsisten

Client menjadi penuh parsing khusus.

41.6 Artifact mutable

Artifact adalah evidence. Kalau bisa diubah, audit trail rusak.

41.7 API mengekspos semua log dan prompt mentah

Ini membuka risiko secret leakage dan policy leakage.


42. Checklist API design

Sebelum lanjut ke database schema, pastikan:

  • Apakah API mengikuti domain model?
  • Apakah command endpoint tidak membypass state machine?
  • Apakah worker API terpisah dari public API?
  • Apakah worker butuh lease token?
  • Apakah semua command endpoint punya idempotency?
  • Apakah error model konsisten?
  • Apakah artifact immutable?
  • Apakah artifact content melewati redaction policy?
  • Apakah scope authz cukup granular?
  • Apakah list endpoint memakai pagination?
  • Apakah OpenAPI menjadi source of truth?
  • Apakah contract test bisa dijalankan di CI?
  • Apakah enum evolution dipikirkan?
  • Apakah raw prompt tidak terekspos sembarangan?
  • Apakah cancellation dan approval adalah command, bukan update field?

43. Ringkasan

OpenAPI-first API design membuat Honk-like AI coding agent menjadi platform, bukan script.

API yang benar:

  • menjaga boundary control plane;
  • membuat task intake eksplisit;
  • mengikat state machine;
  • memisahkan worker capability;
  • menyimpan artifact sebagai evidence;
  • mendukung approval;
  • memaksa idempotency;
  • memberi error model stabil;
  • memungkinkan client dan worker dibuat parallel;
  • menjadi dasar contract test.

Part berikutnya akan masuk ke database schema. Kita akan menerjemahkan Task, Run, Step, Artifact, Patch, VerificationReport, JudgeReport, ApprovalRequest, PullRequestRecord, Event, Lease, dan IdempotencyKey menjadi schema relational yang tahan concurrency dan audit.


References

  • OpenAPI Specification v3.1.1: https://spec.openapis.org/oas/v3.1.1.html
  • OpenAPI Initiative announcement for 3.0.4 and 3.1.1 patch releases: https://www.openapis.org/blog/2024/10/25/announcing-openapi-specification-patch-releases
  • Model Context Protocol specification: https://modelcontextprotocol.io/specification/2025-06-18
  • OpenAI Codex cloud tasks and pull request workflow: https://developers.openai.com/codex/cloud
  • OpenAI Codex sandboxing concept: https://developers.openai.com/codex/concepts/sandboxing
Lesson Recap

You just completed lesson 14 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.