# Job Search API with the OpenAI Agents SDK

Preview: Python 3.12, openai-agents 0.22.3 and Pydantic 2.13.5. Native SDK tests use a scripted Model and local HTTP responses. Real-model selection, live-account authentication, source collection and deployment remain unverified.

Download all five files into one directory: `truefetch_agents.py`, `truefetch_rest.py`, `agent_example.py`, `requirements.txt`, and this README. Create a virtual environment and install `python -m pip install -r requirements.txt`. The source checkout can find the shared engine in `../python`; a standalone download needs its own `truefetch_rest.py`.


## Canonical submission and original-command recovery

Use `POST /v1/quotes`, then `POST /v1/tasks` with `productId`, the approved input, the quoted `expectedPricingVersion`, and the canonical `Idempotency-Key`. The host creates a key **once for explicitly approved new work**, using `new_command_key()` from the bundled REST engine, and persists it with the approved input before submission. Keys have the form `v1.<13-digit Unix milliseconds>.<22–64 base64url characters>`. Do not generate keys in model arguments or a workflow retry expression.

Creation requires `allow_write=True`, the persisted `request_key`, and explicit `command_mode="new"`. The default `command_mode="recover"` performs an original-command GET and never obtains another quote or submits again. Repeated invocation in the same new-intent toolset also reads the original after its first submission attempt. Changed input is refused. After a restart or any lost response, use this host-only path; it needs no input, quote or model call:

```python
import os
from truefetch_rest import recover_command, observed_task_id

original = recover_command(
    os.environ["TRUEFETCH_API_KEY"], "tasks.create", os.environ["TRUEFETCH_COMMAND_KEY"]
)
if original["receipt"]["outcome"] == "accepted":
    saved_task_id = observed_task_id(original)
    # Persist the reference before reading the existing task/results.
```

HTTP 200 is not acceptance. Inspect `receipt.outcome`: `accepted` supplies an authoritative task reference even when the current `task` projection is null; `processing` and `unknown` require another explicit original GET, respecting the returned retry interval. `rejected` and `not_accepted_final` establish nonacceptance and require deliberate new work if you still want a collection. `receipt_expired` remains unconfirmed: inspect Tasks or contact support. The helper never seals, replays a POST or creates a replacement key. The server accepts new keys for 24 hours, allows five minutes of future clock skew, and retains terminal receipts for at least seven days; it can still answer a retained original after the new-command window ends. A current Task state does not establish a particular command's outcome.

Cancellation is a separate command: bind a different persisted canonical `cancel_key` for the newly created task, or pass `cancel_task_ids=(saved_task_id,)` and `cancel_command_keys={saved_task_id: saved_cancel_key}` for an explicitly approved saved target. `POST /v1/tasks/{taskId}/cancel` sends `{}` and that cancellation key. Unknown cancellation is recovered with `recover_command(api_key, "tasks.cancel", saved_cancel_key)`, never the creation key. Acceptance of cancellation does not promise the task ended as cancelled. The budget, keys and cancellation mapping remain host-only arguments.


## Estimate, create and recover

Keep `TRUEFETCH_API_KEY` in the host environment. `OPENAI_API_KEY` is separate and is needed only when your configured model calls OpenAI. Never pass credentials, a request key or budget as model arguments. Start with four read tools; creation and cancellation require explicit host `allow_write=True`, a write-capable key, and a saved request key for one collection intent. Configure API-key monthly limits separately.

```python
import asyncio, json, os
from truefetch_agents import Toolset


async def main():
    tools = Toolset(os.environ["TRUEFETCH_API_KEY"], max_credits=100)
    estimate = next(t for t in tools.tools if t.name == "truefetch_estimate")
    quote = json.loads(
        await estimate.on_invoke_tool(
            None,
            json.dumps(
                {
                    "input": {
                        "keyword": "Software engineer",
                        "country": "Germany",
                        "platforms": ["arbeitnow"],
                        "maxResults": 10,
                        "allowPartial": False,
                    }
                }
            ),
        )
    )
    print(quote)


asyncio.run(main())
```

For collection, persist the approved input and a canonical persisted request key, then construct a new Toolset with `allow_write=True, command_mode="new", request_key=saved_key`. Invoke `truefetch_search_jobs` with that same input. It checks a fresh estimate, credit ceiling and pricing version before submission. Save the accepted receipt's task reference immediately; the current Task projection may be null. Subsequent attempts read the same original receipt; changed input is another intent and must not be silently retried. Creation reserves credits and may charge for collection. One credit is 1,000,000 billing units.

Resume outside the agent with a result-capable key and the saved ID:

```python
result = await tools.recover(saved_task_id, timeout_seconds=120, max_bytes=8 * 1024 * 1024)
# Only this verified complete result is suitable for an all-records export.
assert result["complete"] is True
records = result["data"]
```

Recovery never creates a task. It polls and checks all pages, task identity/status, counts and record ordering; missing pages or inconsistent responses fail. `partial` remains explicit; source outcomes and expiry are in `task`. Complete pagination is not exhaustive source coverage. Persist records through your application with suitable access and retention. Results expire according to their stated retention window.

## Bounded native agent

```python
from agent_example import run_agent, AgentRunError

# configured_model is an SDK Model or an explicitly selected model name.
try:
    reply = await run_agent(
        configured_model,
        "Read the saved task and explain its status.",
        api_key=os.environ["TRUEFETCH_API_KEY"],
        max_credits=100,
        max_turns=8,
        max_tool_calls=16,
        timeout_seconds=120,
    )
except AgentRunError as error:
    saved_handles = error.task_ids
```

The SDK receives native FunctionTools with strict schemas. Optional model fields become required nullable/defaulted fields where the SDK strict-schema conversion requires them. The host validates arguments again before HTTP. The six tools cover usage, estimate, create, task/results, task listing and scoped cancellation; read-only mode omits creation and cancellation. Cancellation only covers IDs created by this toolset or explicitly supplied by the host in `cancel_task_ids`.

The runner disables SDK tracing, limits model turns and local tool invocations, requests sequential model tool calls, and blocks further calls after closing. Model output is an interpretation, not a verified export. Source descriptions remain untrusted text; instructions alone do not prove prompt-injection resistance. Tool arguments and results are sent to the configured model provider; limit their content and configure that provider's retention and logging separately. Model usage charges are independent of TrueFetch credits.

Timeout cancels the awaited runner, not necessarily in-flight thread HTTP or provider processing. HTTP calls have 30-second timeouts. Already-started operations may finish after the runner stops; returned handles can miss late acceptance. Preserve the original request key/input and inspect your workspace after an unknown submission. Never invent a new key to recover it. Use the host-only original GET after response loss; do not submit again to discover acceptance. Deterministic recovery has a cooperative deadline and does not cancel the remote task. No automatic collection retry or cancellation is performed.

Errors expose controlled messages. Authentication, exhausted budget, stale pricing and expired results require inspection. Respect service rate limits. SDK/provider logging is outside the helper's redaction boundary. Do not serialize Toolset internals or credentials into checkpoints; recreate with host secrets and saved non-secret task IDs.

In the source checkout, run `python examples/openai-agents-job-search/test_tools.py` after installation. Tests are not part of the five-file public download and do not need either live API key. Official reference: [OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents/sdk). Checked 2026-09-27; local installed SDK signatures are verified by the tests.

The shared REST reader requires UTF-8 JSON and rejects nonfinite numbers, including numeric overflow. Interrupted or oversized HTTP error bodies keep a redacted HTTP-status diagnostic, and truncated successful transfers fail without returning data. It also rejects duplicate member names within an object, including names written with equivalent Unicode escapes; repeated names in separate records are allowed. Recovery also rejects nonfinite values from tool output instead of marking the export complete. Preserve the saved task ID and original request key after an error; these checks do not create or retry collection.

For each available result page, the reader checks the task again after receiving the page, including the final page. This adds one read-only task GET per page. Changed identity, status, availability or count, or a failed recheck, prevents a complete export. The check is a point-in-time observation; files can expire or become unavailable afterward.

Before collection, the creation tool checks that the estimate matches the requested input and that reserved billing units equal the record limit multiplied by the unit price. Amounts must be positive ASCII integers, and the reservation must fit the host credit budget. Invalid estimates stop before submission; server-side pricing and budget checks still apply.
