Bluestep JS Documentation
    Preparing search index...

    Class Ai

    AI services for BSJS scripts. Every request routes through the web-global usage gate and is metered against the tenant's Bluestep API key. There is no bypass: a call on a caller-supplied key is a call the gate cannot price.

    Index

    Helpers that auto-generate AiTools from existing Relate form metadata. See B.ai.tool.forNewEntry().

    const tool = B.ai.tool.custom()
    .setName("resident_allergies")
    .setDescription("Lists a resident's known allergies")
    .setSchema({ type: "object", properties: { id: { type: "string" } }, required: ["id"] })
    .setExecutor((args) => allergiesFor(args.id).join(", "));
    • Open a reusable AI agent that maintains connection settings, an accumulated tool registry, an onTurn callback, the per-iteration hooks, and a running transcript across multiple sendMessage(...) calls. Each sendMessage dispatches through the same usage-gated pipeline as B.ai.call and keeps the returned transcript so subsequent calls continue the conversation.

      The hooks (setOnRequest, setOnToolCall, setOnToolResult, setOnIteration, setOnDelta, or the same names on the options literal) run between the model's iterations: inspect each request, run, deny or defer each tool call, reshape each result, stop the loop, or read the answer as it streams. A deferred call is answered later with resume(results), in this execution or a later one.

      const tool = B.ai.tool.custom()
      .setName("add_1")
      .setDescription("adds one!")
      .setSchema({
      type: "object",
      properties: { a: { type: "integer" }, b: { type: "integer" } },
      required: ["a", "b"],
      })
      .setExecutor((args) => args.a + args.b + 1);

      const agent = B.ai.agent()
      .addTool(tool)
      .setProvider("anthropic")
      .setModel("claude-opus-4-8")
      .setSystemPrompt("You are a helpful assistant.");

      const r1 = agent.sendMessage("What is 2 + 3?");
      const r2 = agent.sendMessage("Now add 10 to that.");

      Returns AiAgent

    • Open a reusable AI agent pre-seeded from options. Equivalent to calling the no-arg agent() and then chaining the corresponding setters / addTool calls — every field on AiClientOptions is copied straight into the agent. After construction, the agent behaves identically to one built via the fluent setters: each sendMessage(...) dispatches through the same usage-gated pipeline as B.ai.call and threads the user message and assistant response into the running chatHistory.

      That includes repos: each entry is mounted when a call begins, pinned to one commit, and offered to the model as an alias and nothing else. Use addRepo / removeRepo to change the set afterwards, and read result.mounts to record which trees an answer was computed from.

      const tool = B.ai.tool.custom()
      .setName("add_1")
      .setDescription("adds one!")
      .setSchema({
      type: "object",
      properties: { a: { type: "integer" }, b: { type: "integer" } },
      required: ["a", "b"],
      })
      .setExecutor((args) => args.a + args.b + 1);

      const agent = B.ai.agent({
      provider: "anthropic",
      model: "claude-opus-4-8",
      systemPrompt: "You are a helpful assistant.",
      tools: [tool],
      });

      const r1 = agent.sendMessage("What is 2 + 3?");
      const r2 = agent.sendMessage("Now add 10 to that.");

      Type Parameters

      Parameters

      Returns AiAgent

    • Sends a request to an AI provider and returns the response.

      The call is metered through the web-global AI usage gate under the tenant's Bluestep API key, with usage attributed to the calling user, the current operating Unit, the optional flag, the model, and the script that triggered the call. Every call goes through the gate — there is no bypass, because a call on a caller-supplied key is a call the gate cannot price.

      When an onTurn callback is supplied, the conversation continues for as many assistant↔user exchanges as the callback drives — analogous to a state-machine agent loop but owned by the script.

      message is a string, or an array of content blocks when the model needs to see something other than words — an image, a PDF, or a file read straight out of a repo mounted through repos:

      const result = B.ai.call({
      provider: "anthropic",
      repos: [{ purpose: "general", repo: "handbook", alias: "handbook",
      preload: ["policies/**"] }],
      message: [
      { type: "text", text: "Does this scan match the policy?" },
      { type: "image", source: entry.fields.intakeScan },
      { type: "text", repo: "handbook", path: "policies/intake.md" },
      ],
      });

      Repo-sourced content is read at the mount's pinned commit and stored in the transcript as a reference rather than as bytes, so a checkpoint stays small and a later call re-reads the same version — mount with ref set to the pinnedSha from result.mounts to resume one. preload feeds a mount's matching files in before the model's first turn, which saves an agent several opening turns of reading them one git_read at a time; result.mounts[].preloaded says which files that was. Content the script holds inline — a document field, a stream, base64 — has no source to re-read, so a transcript carrying one cannot be serialized.

      Set trace: true and result.trace holds one entry per model round-trip — what each one spent, how long it took, which tool calls it asked for, and anything the provider would not send as written. The counts are per round-trip rather than running totals, so the iteration that cost the tokens is the one with the large number. Every call also writes one ai.call line to the organization log, whether or not it traced: search it for the trackingId on the result to find a run after the fact.

      The type parameter Schemas is inferred from the tools tuple's input_schema literals (preserved by the const modifier), so inline tool literals get a typed executor args parameter — typos in argument names and wrong-shape access fail at compile time, matching the safety of B.ai.tool.custom().

      Type Parameters

      Parameters

      Returns AiCallResult

    • Sets the AI budget for the calling tenant. Superuser only. Upserts the ai_tenant_config row for the (schema, unitId, flag) scope — the schema comes from the operating context; unitId and flag are optional (omit for the tenant-wide default, a null wildcard on that dimension). maxIterations (the per-call iteration cap) is required and positive on every call, enable: false included. maxSpendMicros (micro-dollars; 1_000_000 = $1.00) is required and positive when enabling; omitting it while disabling provisions an uncapped-but-metered scope. budgetSchedule is one of HOURLY, DAILY, WEEKLY, MONTHLY, LIFETIME (default LIFETIME = cumulative cap); utcOffsetMinutes anchors windowed schedules to the tenant's local calendar. Fail-closed: any error throws.

      Parameters

      Returns void

      B.ai.configure({
      maxIterations: 12,
      maxSpendMicros: 50000000, // $50.00
      budgetSchedule: "MONTHLY",
      utcOffsetMinutes: -420,
      });
    • Count the input tokens a conversation occupies, without sending it.

      Takes a transcript or a plain string. Answers {tokens, estimated} — estimated is false when the provider counted it and true when no token-counting API was available and the harness estimated from the text instead.

      const store = B.ai.state(curEntry.fields.agentState);
      const agent = B.ai.agent({provider: "anthropic"});
      agent.sendMessage(B.net.request.parameter("q"));

      const size = B.ai.countTokens(agent.getTranscript());
      if (!size.estimated && size.tokens > 150000) {
      store.save(agent); // park it before the window runs out
      }

      An estimate is rough — code, JSON and non-Latin text tokenize more densely than it assumes — so budget against one with room to spare rather than to the token.

      Parameters

      • input: string | AiTranscript

        The conversation, or a single run of text.

      • Optionaloptions: AiCountTokensOptions

        Which model to count against. Omit for the configured default.

      Returns AiTokenCount

    • Open a handle that keeps an agent's conversation on a Relate field, so it survives the end of this execution.

      The field is a memo field or a serializable field — both hold a transcript, where a text field's length limit does not. An unsupported field is refused here, when the handle is opened, rather than at the first save(...).

      const store = B.ai.state(curEntry.fields.agentState);
      const agent = B.ai.agent({tools: [proposeEntry], systemPrompt: PROMPT});

      if (store.restore(agent)) {
      // A conversation was waiting: answer the calls it deferred last time.
      agent.resume(reviewed.map(r => ({id: r.id, result: r.outcome})));
      } else {
      agent.sendMessage(B.net.request.parameter("q"));
      }
      store.save(agent);

      restore answers false on the first run and leaves the agent alone, so a script can tell a fresh conversation from a resumed one without reading the field itself.

      Parameters

      Returns AiState

    • Open a streaming AI agent for realtime audio (OpenAI only). Shares the connection setters and tool registry of agent(), but instead of sendMessage(text) it exposes sendAudio(...): audio is streamed straight to the model over a live session, the model may call the registered tools (executed in-script, results fed back automatically), and the final response is returned as an AiCallResult.

      Each sendAudio(...) is a one-shot turn — there is no running chatHistory or onTurn loop. Audio must already be PCM16 @ 24kHz mono.

      const tool = B.ai.tool.custom()
      .setName("lookup")
      .setDescription("look up a record")
      .setSchema({ type: "object", properties: { id: { type: "string" } }, required: ["id"] })
      .setExecutor((args) => B.find.record(args.id)?.name ?? "not found");

      // e.g. a live PCM16 mic upload posted to a BSJS endpoint
      B.net.request.binaryStream((audioIn) => {
      const result = B.ai.streamingAgent()
      .addTool(tool)
      .setSystemPrompt("You are a voice assistant. Use tools to answer.")
      .sendAudio(audioIn);
      B.net.response.contentType("application/json");
      B.net.response.stream(out => out.print(JSON.stringify({
      text: result.text,
      outcome: result.outcome
      })));
      });

      Returns StreamingAiAgent

    • Decode a transcript a previous call stored with AiTranscript.toJson(), to inspect it through entries() or to hand it to B.ai.call({transcript}) / agent.setTranscript(...). Both of those accept the string directly, so this is only needed to read a stored transcript without making a call.

      Throws when the string is not a transcript this version reads, or when its tool results do not pair with tool calls; the message names the entry at fault.

      Parameters

      • json: string

        What AiTranscript.toJson() returned.

      Returns AiTranscript

      const json = curEntry.fields.agentState.value;     // stored by an earlier execution
      const result = B.ai.call({ transcript: B.ai.transcript(json), message: "And the dose?" });
    • Returns current AI spend against the configured budget for the calling tenant, targeting the tenant-wide default scope. Superuser only. Equivalent to calling usageReporting(options) with no scope filters. spendMicrosUsed is the sum within the current budget window; windowEnd and the limit fields are null when the scope is unconfigured (soft rollout — unlimited but still tracked) or on a LIFETIME schedule.

      Returns AiUsageReport

      const report = B.ai.usageReporting();
      B.out = (report.spendMicrosUsed / 1000000).toFixed(2) + " spent this window";
    • Returns current AI spend against the configured budget for the calling tenant. Superuser only. The tenant schema is taken from the operating context unless options.schemaName overrides it; unitId and flag narrow the scope (omit both for the tenant-wide default). Passing options.schemaName names another tenant, so it requires a restricted script running with unscoped super-user authority — a reseller-scoped super is refused. spendMicrosUsed is the sum within the current budget window; windowEnd and the limit fields are null when the scope is unconfigured (soft rollout — unlimited but still tracked) or on a LIFETIME schedule.

      Parameters

      • Optionaloptions: AiUsageReportingOptions

        Scope filters. All optional — omit unitId/flag for the tenant-wide default; schemaName overrides the tenant schema (restricted).

      Returns AiUsageReport

      const report = B.ai.usageReporting({ flag: "intake-agent" });
      const nearLimit = report.maxSpendMicros !== null
      && report.spendMicrosUsed > report.maxSpendMicros * 0.9;
    • Returns Ai