# Job Search API with Vercel AI SDK

Preview: Node.js 24, `ai` 7.0.118 and `zod` 4.6.5. Native SDK tests use MockLanguageModelV3 and a local HTTP server. Real models, live credentials and real-source collection remain unverified. This is a server-side example, not a browser bundle or published package.

Save `truefetch.mjs`, `agent.mjs`, `package.json`, `package-lock.json` and this README in one directory. Run `npm ci --ignore-scripts`. Supply your own configured AI SDK model/provider and its credentials separately. No provider package or default model is selected by this example.


## Canonical submission and original-command recovery

The host creates `newCommandKey()` **once for explicitly approved new work**, then persists the key and approved input before submission. Its canonical form is `v1.<13-digit Unix milliseconds>.<22–64 base64url characters>`. Construct a writer with `allowWrite: true, commandMode: 'new', requestKey: savedKey`. Creation calls `POST /v1/quotes`, validates the quote, then sends `POST /v1/tasks` with `productId`, input, quoted `expectedPricingVersion`, and `Idempotency-Key`. The default `commandMode: 'recover'` and repeated invocation after the first submission attempt only read the original; they never requote or send a replacement POST. Changed input is refused.

```js
import { createToolset } from './truefetch.mjs';
const reader = createToolset({apiKey: process.env.TRUEFETCH_API_KEY, maxCredits: 100});
try {
  const original = await reader.recoverCommand('tasks.create', process.env.TRUEFETCH_COMMAND_KEY);
  if (original.receipt.outcome === 'accepted') {
    const savedTaskId = original.receipt.result.objects.find(ref => ref.kind === 'task').id;
    // Persist the reference before reading the existing task/results.
  }
} finally { reader.close(); }
```

Original recovery needs no input, quote or model call. HTTP 200 is not acceptance. An `accepted` receipt supplies the actual task reference even when the current Task projection is null. `processing` and `unknown` remain unconfirmed; explicitly GET the original again after its retry interval. `rejected` and `not_accepted_final` establish nonacceptance and allow a deliberate new intent. `receipt_expired` remains unconfirmed: inspect Tasks or contact support. Never infer an original command's outcome from the current Task status. The helper does not seal or replay POST. New-command acceptance lasts 24 hours with five minutes of future clock allowance; terminal receipts remain for at least seven days, and retained originals can be read after the new-command window ends.

Cancellation has a separate persisted canonical key. Supply `cancelKey: savedCancelKey` for the newly created task, or `cancelTaskIds: [savedTaskId], cancelCommandKeys: {[savedTaskId]: savedCancelKey}` for an approved existing target. Cancellation sends `POST /v1/tasks/{taskId}/cancel` with `{}` and that key; recover uncertainty with `reader.recoverCommand('tasks.cancel', savedCancelKey)`. Accepted cancellation does not promise a cancelled final Task. Keys and target scope remain host-only and never enter model tool arguments.


## Estimate and collect

```js
import { createToolset } from './truefetch.mjs';
const bundle = createToolset({
  apiKey: process.env.TRUEFETCH_API_KEY, maxCredits: 100,
});
const input = { keyword: 'Software engineer', country: 'Germany', platforms: ['arbeitnow'], maxResults: 10, allowPartial: false };
const quote = await bundle.tools.truefetch_estimate.execute({ input });
```

Four read tools expose account usage, estimates, a saved task with one result page, and task listing. To create a task, save the approved input and a canonical persisted request key, then construct a toolset with `allowWrite: true, commandMode: 'new', requestKey: savedKey`. Call `truefetch_search_jobs.execute({input})` and persist the accepted receipt's task reference; `result.task` can be null. Budget and request key belong to the host; the model cannot pass or raise them in arguments. Set API-key monthly limits separately.

Creation checks a fresh estimate, pricing version and the host credit ceiling. One credit is 1,000,000 billing units. Collection reserves credits and may incur charges; model/provider charges are separate. Further attempts read the same original receipt without another quote or POST. Do not generate retry keys from time, randomness or model output. Cancellation is available only for tasks created by this toolset or explicitly passed in `cancelTaskIds`; a read does not authorize cancellation.

## Recover all records

```js
const result = await bundle.recover(savedTaskId, {
  timeoutMs: 120_000, pollMs: 2_000, maxBytes: 8 * 1024 * 1024,
});
// Only use this deterministic complete result for an all-records export.
const { task, data, complete, partial } = result;
```

Recovery is host-side and never starts collection. It checks identity, successful terminal status, availability, each page's count, cursor progress and record ordering, and rereads the task receipt after every page. Missing pages, changed receipts and excessive output fail without returning a complete export. Partial and empty results remain explicit. Source outcomes and retention expiry are in `task`; complete pagination does not imply exhaustive source coverage. Use a result-capable key and a new toolset to resume after a process restart.

The total recovery deadline is 1–300,000 ms, polling interval 1–30,000 ms and output bound 1 byte–32 MiB. Recovery checks elapsed time with a monotonic clock before and after receipt and page reads and before returning a complete export; a late response is rejected even if the timeout callback has not run yet. Each HTTP call has a 30-second deadline and an 8 MiB response limit. Cancellation stops application waits even if the transport ignores its signal. Responses are copied into a fixed byte buffer and decoded as strict UTF-8; malformed bytes fail. Numeric overflow in JSON is rejected at every nesting level before a tool response or complete export is returned. Duplicate member names in the same object, including equivalent Unicode escapes, are also rejected. Names shared by separate records are allowed. Reader locks and timers are released without waiting for transport cleanup. The serialized output bound is not a bound on all process memory. Responses and source text are untrusted data. No redirects or automatic collection retries are allowed.

## Run the native agent

```js
import { runAgent, AgentRunError } from './agent.mjs';
try {
  const result = await runAgent({
    model: configuredModel, prompt: 'Read the saved task and explain its status.',
    apiKey: process.env.TRUEFETCH_API_KEY, maxCredits: 100,
    maxSteps: 8, maxToolCalls: 16, timeoutMs: 120_000,
  });
} catch (error) {
  if (error instanceof AgentRunError) {
    // Retain error.taskIds and the original request key/input for recovery.
  } else throw error;
}
```

The runner uses `ToolLoopAgent` with native `tool` schemas, stops on a tool error or step limit, disables SDK telemetry and model retries, and closes tools after the run. Tool-call caps also apply to parallel calls. A step limit can return only task handles with no final text; it is not proof that the user's task is complete. Model output is an interpretation, not a verified export. Call `recover` outside the model loop.

HTTP requests receive the abort signal. Aborting local waiting cannot reverse an accepted remote collection or guarantee the model provider has stopped processing. Late acceptance can be absent from the returned task IDs. Persist the original key/input before enabling writes and inspect your workspace when submission is unknown. Recover an uncertain submission through the original GET, which does not need a quote. Authentication, budget, stale pricing, rate limits and expired results need inspection; do not enable blanket retry. Retained results can expire.

Keep this code and secrets on the server. Tool arguments and results go to your configured model provider; control access, retention and provider logs separately. Disabled SDK telemetry does not change provider policies. Instructions alone do not establish prompt-injection resistance. `fetchImpl` is a trusted host test seam, never a user/model input; use the default fetch in production. Do not serialize tool closures or credentials into checkpoints.

From the source checkout, run `npm test` in this example directory after installing dependencies. Test files are not in the public download. Official references: [Agents](https://ai-sdk.dev/docs/agents/building-agents), [SDK testing](https://ai-sdk.dev/docs/ai-sdk-core/testing). No deployment is performed by these commands.

Call `bundle.close()` when the host run ends. It stops pending HTTP waits and recovery polling and rejects later calls. A quote that arrives after closing cannot start collection. Closing does not cancel an already submitted server task or undo its charge; retain the original input/key and observed task IDs to check its outcome with a new toolset.

`runAgent` accepts an optional host `signal` and enforces `timeoutMs` while waiting for the model. Pre-cancelled runs make no model call; late answers are discarded. `AgentRunError.taskIds` retains already observed handles. Stopping the host wait does not guarantee that the model provider has stopped processing or billing, and an already submitted collection must still be checked with its original key.

Creation also verifies 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 within the host budget. Invalid estimates stop before the collection POST; server-side pricing and budget checks still apply.
