@oneshot-agent/mcp-server 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,12 +79,12 @@ Every tool is named `oneshot_<name>`. Below are the names.
79
79
  | [Talk](https://docs.oneshotagent.com/api-reference/email/send) | `email` `voice` `sms` `inbox_list` `inbox_get` `sms_inbox_list` `sms_inbox_get` `notifications` `mark_notification_read` |
80
80
  | [Inbound calls](https://docs.oneshotagent.com/api-reference/voice/inbound) | `voice_number_provision` `voice_numbers` `voice_inbound_set` `voice_inbound_disable` `voice_inbound_calls` |
81
81
  | [Send from your domains](https://docs.oneshotagent.com/api-reference/email/domains) | `list_domains` `pause_domain` `resume_domain` |
82
- | [Find people and companies](https://docs.oneshotagent.com/api-reference/research/person-find) | `person_find` `person_profile` `person_email` `person_email_status` `person_dossier` `person_social_accounts` `person_articles` `person_posts` `person_interests` `person_followers` `company_find` `company_profile` `business_find` `business_resolve` `record_solicitations_find` |
82
+ | [Find people and companies](https://docs.oneshotagent.com/api-reference/research/person-find) | `person_find` `person_profile` `person_email` `person_email_status` `person_dossier` `person_social_accounts` `person_articles` `person_posts` `person_interests` `person_followers` `company_find` `company_profile` `company_dossier` `business_find` `business_resolve` `record_solicitations_find` |
83
83
  | [Lookups](https://docs.oneshotagent.com/tools-and-goals#lookups) | public records, places, products, vehicles, genes, proteins and more, one tool each |
84
84
  | [Think](https://docs.oneshotagent.com/api-reference/research) | `research` `predict` `solve` |
85
85
  | [Web and buying](https://docs.oneshotagent.com/api-reference/web-search) | `web_search` `web_read` `browser` `product_find` `commerce_buy` |
86
86
  | [Physical mail](https://docs.oneshotagent.com/api-reference/physical-mail/overview) | `physical_mail_upload` `physical_mail_validate_address` `physical_mail_preview` `physical_mail_quote` `physical_mail_approve` `physical_mail_send` `physical_mail_recover` `physical_mail_order` `physical_mail_cancel` |
87
- | [Connected systems](https://docs.oneshotagent.com/api-reference/connections/overview) | `connections_list` `ledger_invoices` `ledger_bills` `ledger_reports` `crm_accounts` `crm_contacts` `crm_opportunities` `tax_rates` `tax_calculate` `shipping_shipments` `shipping_tracking` `shipping_rates` `procurement_transactions` `procurement_bills` `procurement_suppliers` |
87
+ | [Connected systems](https://docs.oneshotagent.com/api-reference/connections/overview) | `connections_list` `ledger_invoices` `ledger_invoice_lines` `ledger_bills` `ledger_reports` `ledger_bank_transactions` `crm_accounts` `crm_contacts` `crm_opportunities` `crm_tickets` `crm_activity` `tax_rates` `tax_calculate` `shipping_shipments` `shipping_tracking` `shipping_rates` `procurement_transactions` `procurement_bills` `procurement_suppliers` `meetings_list` `meetings_get` `field_service_clients` `field_service_jobs` `field_service_quotes` `field_service_invoices` |
88
88
  | [LinkedIn](https://docs.oneshotagent.com/api-reference/linkedin/overview) | `linkedin_connect` `linkedin_accounts` `linkedin_invite` `linkedin_message` `linkedin_reply` and more |
89
89
  | [Build](https://docs.oneshotagent.com/api-reference/build/create) | `build` `update_build` |
90
90
  | [Goals](https://docs.oneshotagent.com/api-reference/compute/create) | `compute` `compute_status` `compute_tasks` `compute_respond` `compute_pause` `compute_resume` `compute_fund` `compute_cancel` |
@@ -110,9 +110,10 @@ wallet. The server signs it for you. A read with no proof and no valid token is
110
110
  ## Tool notes
111
111
 
112
112
  - **Email.** Omit `from_domain` and `from_mailbox` and the send rotates across
113
- your warmed domains. `from_mailbox` needs `from_domain`. A new `from_domain`
114
- only quotes; nothing is charged until you confirm. `mailbox_mode` applies only
115
- when a domain is first set up. [Details](https://docs.oneshotagent.com/api-reference/email/send).
113
+ your domains, each from its main address. `from_mailbox` needs `from_domain`
114
+ and must be one of the domain's addresses. A new `from_domain` or address only
115
+ quotes its setup; nothing is charged or sent. The owner picks the domain's mode
116
+ and addresses when buying it. [Details](https://docs.oneshotagent.com/api-reference/email/send).
116
117
  - **Phone lookup** is off by default on `person_find`, `person_profile` and
117
118
  `person_dossier`. `include_phone: true` is charged for each person you ask for
118
119
  (the search limit, default 100), or once for a profile or dossier, found or not.
@@ -149,7 +150,7 @@ wallet. The server signs it for you. A read with no proof and no valid token is
149
150
 
150
151
  ## Tool discovery
151
152
 
152
- The MCP catalog contains 193 tools. `oneshot_discover_tools` is a free, read-only
153
+ The MCP catalog contains 243 tools. `oneshot_discover_tools` is a free, read-only
153
154
  search over the tools your token can call. Call it with no arguments for category
154
155
  counts, `{ "query": "send a printed letter" }` to search, or `{ "category": "email" }`
155
156
  to browse. Results contain public tool names, purposes and required inputs.
package/dist/index.js CHANGED
@@ -50,7 +50,7 @@ async function main() {
50
50
  const agent = await initAgent();
51
51
  const server = new Server({
52
52
  name: "oneshot-mcp",
53
- version: "0.49.0", // keep in sync with package.json; guarded by version.test.ts
53
+ version: "0.51.0", // keep in sync with package.json; guarded by version.test.ts
54
54
  }, {
55
55
  capabilities: {
56
56
  tools: {},
@@ -6,8 +6,10 @@ import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
6
6
  *
7
7
  * readOnlyHint true = retrieval only (paid lookups included — the only
8
8
  * side effect is our own billing)
9
- * destructiveHint true = deletes, overwrites or revokes something. Sends are
10
- * additive writes, so false — like any mail app's send.
9
+ * destructiveHint true = deletes, overwrites or revokes something, or is an
10
+ * irreversible send or transaction (a message, call, letter
11
+ * or purchase cannot be taken back). OpenAI's plugin
12
+ * guidelines ask for true on those.
11
13
  * openWorldHint true = reaches the public web or third parties;
12
14
  * false = only the caller's own OneShot account data.
13
15
  */
@@ -1,38 +1,47 @@
1
1
  import { LOOKUP_MANIFEST, ROUTE_MANIFEST } from "./lookup-manifest.generated.js";
2
2
  /** Retrieval only. */
3
3
  const read = (title, openWorld) => ({ title, readOnlyHint: true, destructiveHint: false, openWorldHint: openWorld });
4
- /** Changes something. `destructive` only for delete / overwrite / revoke. */
4
+ /** Changes something. `destructive` for delete / overwrite / revoke and for irreversible sends and transactions. */
5
5
  const write = (title, openWorld, destructive = false) => ({ title, readOnlyHint: false, destructiveHint: destructive, openWorldHint: openWorld });
6
+ /** Declared routes that cannot be taken back or overwrite settings (a watch's cap and webhook), so destructive. */
7
+ const IRREVERSIBLE_ROUTES = new Set(["physical_mail_approve", "physical_mail_cancel", "identity_start", "watch_update"]);
8
+ /**
9
+ * Free declared routes that still reach outside the account, so open-world: approving a mail quote posts a
10
+ * letter, an identity check sends a person a link, a watch follows third-party data and may call a webhook,
11
+ * and a browser account connection signs in to another site.
12
+ */
13
+ const OUTBOUND_ROUTES = new Set(["physical_mail_approve", "identity_start", "watch", "watch_update", "browser_connect_account"]);
6
14
  /** The connected-system reads (#2403): paid, but each reads the caller's own connected business system, so closed-world. */
7
- const CONNECTED_SYSTEM_READ = /^\/v1\/tools\/(ledger|crm|shipping|tax|procurement)\//;
15
+ const CONNECTED_SYSTEM_READ = /^\/v1\/tools\/(ledger|crm|shipping|tax|procurement|meetings|mailbox|files|field_service)\//;
8
16
  const OPEN = true;
9
17
  const OWN = false;
10
18
  const DESTRUCTIVE = true;
11
19
  export const TOOL_ANNOTATIONS = {
12
20
  oneshot_discover_tools: read("Discover tools", OWN),
21
+ oneshot_lookup: read("Run a lookup", OPEN),
13
22
  oneshot_solve: read("Solve", OWN),
14
23
  oneshot_predict: read("Predict", OWN),
15
24
  oneshot_hunt_list: read("List Soul Hunts", OPEN),
16
25
  oneshot_hunt_status: read("Get Soul Hunt Status", OPEN),
17
- oneshot_hunt_predict: write("Predict Soul Hunt", OPEN),
26
+ oneshot_hunt_predict: write("Predict Soul Hunt", OPEN, DESTRUCTIVE), // a submitted prediction cannot be withdrawn
18
27
  oneshot_hunt_collection: read("List Collected Souls", OWN),
19
28
  oneshot_hunt_leaderboard: read("Soul Hunt Leaderboard", OPEN),
20
- oneshot_email: write("Send email", OPEN),
21
- oneshot_voice: write("Place a phone call", OPEN),
22
- oneshot_sms: write("Send a text message", OPEN),
29
+ oneshot_email: write("Send email", OPEN, DESTRUCTIVE),
30
+ oneshot_voice: write("Place a phone call", OPEN, DESTRUCTIVE),
31
+ oneshot_sms: write("Send a text message", OPEN, DESTRUCTIVE),
23
32
  oneshot_inbox_list: read("List received emails", OWN),
24
33
  oneshot_inbox_get: read("Read a received email", OWN),
25
34
  oneshot_sms_inbox_list: read("List received texts", OWN),
26
35
  oneshot_sms_inbox_get: read("Read a received text", OWN),
27
- oneshot_voice_number_provision: write("Get a phone number", OPEN),
36
+ oneshot_voice_number_provision: write("Get a phone number", OPEN, DESTRUCTIVE),
28
37
  oneshot_voice_numbers: read("List phone numbers", OWN),
29
38
  oneshot_voice_inbound_set: write("Set up call answering", OPEN, DESTRUCTIVE), // replaces the number's answering settings; outside callers reach it via the carrier
30
- oneshot_voice_inbound_disable: write("Stop answering calls", OPEN),
39
+ oneshot_voice_inbound_disable: write("Stop answering calls", OPEN, DESTRUCTIVE),
31
40
  oneshot_voice_inbound_calls: read("List answered calls", OWN),
32
41
  // Physical Mail (#1883): the upload stores artwork in the agent's own account; the send puts a letter in the post.
33
42
  // The generated route tools (validate, preview, quote, approve, recover, order, cancel) get their hints from the manifest below.
34
43
  oneshot_physical_mail_upload: write("Upload mail artwork", OWN),
35
- oneshot_physical_mail_send: write("Send approved physical mail", OPEN),
44
+ oneshot_physical_mail_send: write("Send approved physical mail", OPEN, DESTRUCTIVE),
36
45
  oneshot_research: read("Deep research", OPEN),
37
46
  oneshot_person_find: read("Find people", OPEN),
38
47
  oneshot_person_profile: read("Look up a person's profile", OPEN),
@@ -51,7 +60,7 @@ export const TOOL_ANNOTATIONS = {
51
60
  oneshot_business_resolve: read("Look up a local business", OPEN),
52
61
  oneshot_record_solicitations_find: read("Find government contracts", OPEN),
53
62
  oneshot_product_find: read("Search products", OPEN),
54
- oneshot_commerce_buy: write("Buy a product", OPEN),
63
+ oneshot_commerce_buy: write("Buy a product", OPEN, DESTRUCTIVE),
55
64
  oneshot_compute: write("Start a goal", OPEN),
56
65
  oneshot_compute_status: read("Check a goal", OWN),
57
66
  oneshot_compute_tasks: read("List a goal's tasks", OWN),
@@ -59,8 +68,8 @@ export const TOOL_ANNOTATIONS = {
59
68
  oneshot_compute_respond: write("Answer a goal's question", OWN),
60
69
  oneshot_compute_pause: write("Pause a goal", OWN),
61
70
  oneshot_compute_resume: write("Resume a goal", OWN),
62
- oneshot_compute_fund: write("Add budget to a goal", OWN),
63
- oneshot_build: write("Build a website", OPEN),
71
+ oneshot_compute_fund: write("Add budget to a goal", OWN, DESTRUCTIVE),
72
+ oneshot_build: write("Build a website", OPEN, DESTRUCTIVE), // publishes a live site and is paid
64
73
  oneshot_update_build: write("Update a website", OPEN, DESTRUCTIVE), // overwrites the live site
65
74
  oneshot_browser: write("Browse and act on a website", OPEN),
66
75
  oneshot_browser_create_profile: write("Create a browser profile", OWN),
@@ -72,14 +81,14 @@ export const TOOL_ANNOTATIONS = {
72
81
  oneshot_linkedin_sync: write("Sync LinkedIn messages", OPEN), // paid; imports history into the account
73
82
  oneshot_linkedin_conversations: read("List LinkedIn conversations", OWN),
74
83
  oneshot_linkedin_messages: read("Read LinkedIn messages", OWN),
75
- oneshot_linkedin_reply: write("Reply in a LinkedIn conversation", OPEN),
76
- oneshot_linkedin_message: write("Message a LinkedIn connection", OPEN),
84
+ oneshot_linkedin_reply: write("Reply in a LinkedIn conversation", OPEN, DESTRUCTIVE),
85
+ oneshot_linkedin_message: write("Message a LinkedIn connection", OPEN, DESTRUCTIVE),
77
86
  oneshot_linkedin_invitation: read("Check a LinkedIn invitation", OWN),
78
87
  oneshot_linkedin_invitations: read("List LinkedIn invitations", OWN),
79
- oneshot_linkedin_invite: write("Send a LinkedIn invitation", OPEN),
88
+ oneshot_linkedin_invite: write("Send a LinkedIn invitation", OPEN, DESTRUCTIVE),
80
89
  oneshot_linkedin_withdraw_invitation: write("Withdraw a LinkedIn invitation", OPEN, DESTRUCTIVE),
81
90
  oneshot_linkedin_view_profile: write("View a LinkedIn profile", OPEN), // notify=true is visible to the person
82
- oneshot_linkedin_react: write("React to a LinkedIn post", OPEN),
91
+ oneshot_linkedin_react: write("React to a LinkedIn post", OPEN, DESTRUCTIVE),
83
92
  oneshot_list_domains: read("List sending domains", OWN),
84
93
  oneshot_pause_domain: write("Pause a sending domain", OWN),
85
94
  oneshot_resume_domain: write("Resume a sending domain", OWN),
@@ -92,7 +101,7 @@ export const TOOL_ANNOTATIONS = {
92
101
  oneshot_mark_notification_read: write("Mark a notification read", OWN),
93
102
  oneshot_get_balance: read("Check balance", OWN),
94
103
  oneshot_budget_status: read("Check budget", OWN),
95
- oneshot_approval_create: write("Ask a person to approve", OPEN),
104
+ oneshot_approval_create: write("Ask a person to approve", OPEN, DESTRUCTIVE),
96
105
  oneshot_approval_get: read("Check an approval", OWN),
97
106
  oneshot_approval_list: read("List approvals", OWN),
98
107
  oneshot_approval_cancel: write("Cancel an approval", OWN, DESTRUCTIVE),
@@ -105,10 +114,10 @@ export const TOOL_ANNOTATIONS = {
105
114
  // read (Changes, Benchmark) reaches recorded third-party data, a connected-system read the caller's own system; a
106
115
  // write changes something, and a DELETE removes it.
107
116
  ...Object.fromEntries(ROUTE_MANIFEST.map((entry) => {
108
- const own = !entry.paid || CONNECTED_SYSTEM_READ.test(entry.path);
117
+ const own = (!entry.paid || CONNECTED_SYSTEM_READ.test(entry.path)) && !OUTBOUND_ROUTES.has(entry.name);
109
118
  return [
110
119
  `oneshot_${entry.name}`,
111
- entry.readOnly ? read(entry.title, own ? OWN : OPEN) : write(entry.title, own ? OWN : OPEN, entry.method === "DELETE"),
120
+ entry.readOnly ? read(entry.title, own ? OWN : OPEN) : write(entry.title, own ? OWN : OPEN, entry.method === "DELETE" || IRREVERSIBLE_ROUTES.has(entry.name)),
112
121
  ];
113
122
  })),
114
123
  };
@@ -20,11 +20,6 @@ export declare function handleComputeCancel(agent: OneShot, args: Record<string,
20
20
  export declare function handleComputeRespond(agent: OneShot, args: Record<string, unknown>): Promise<{
21
21
  goal_id: string;
22
22
  status: string;
23
- end_reason: string | null;
24
- dispute: {
25
- at: string;
26
- reason: string;
27
- };
28
23
  } | {
29
24
  task_id: string;
30
25
  goal_id: string;
@@ -1,6 +1,30 @@
1
+ const KNOWN_GOAL_TOOL = { type: "string", description: 'A OneShot tool name, e.g. "email", "voice", "commerce_buy"' };
2
+ const SCOPE_DRIVERS = { type: "object", additionalProperties: { anyOf: [{ type: "number" }, { type: "boolean" }, { type: "string" }] }, description: 'Request sizes, never prices: recipients, segments, max_duration_minutes, max_steps (e.g. {"max_steps":25}), include_phone and limit, or letter kind/pages/color/double_sided; Predict/Solve size and time_limit_s. Omitted fields use tool defaults.' };
3
+ const SCOPE_USE = {
4
+ type: "object",
5
+ properties: {
6
+ tool: KNOWN_GOAL_TOOL,
7
+ qty: { type: "integer", minimum: 1, description: "How many times inside one unit" },
8
+ max_attempts: { type: "integer", minimum: 1, description: "Attempts each use may take" },
9
+ drivers: SCOPE_DRIVERS,
10
+ target: { type: "string", description: 'Whom or what the use is pointed at, e.g. "the carrier\'s billing desk"' },
11
+ persistence: {
12
+ type: "object",
13
+ description: "How long a use keeps trying to reach the world, and what happens when it cannot",
14
+ properties: {
15
+ max_attempts: { type: "integer", minimum: 1 },
16
+ spacing_seconds: { type: "integer", minimum: 0, description: "Seconds between two attempts at the same target" },
17
+ escalate_to: KNOWN_GOAL_TOOL,
18
+ then: { type: "string", enum: ["owner", "give_up"], description: "After the last attempt: back to you, or given up as unreached" },
19
+ },
20
+ required: ["max_attempts", "spacing_seconds", "then"],
21
+ },
22
+ },
23
+ required: ["tool", "qty", "max_attempts"],
24
+ };
1
25
  export const computeTool = {
2
26
  name: "oneshot_compute",
3
- description: "Create an autonomous compute goal: give OneShot an objective and a budget; it plans and runs the steps across OneShot tools. Returns a goal id to poll. Paid tool (quote-to-pay against the budget).",
27
+ description: "Create an autonomous compute goal: give OneShot an objective and a budget; it plans and runs the steps across OneShot tools. Name the unit of work, how work starts (a given list, an event, a schedule, or once) and what each unit needs, or pass `scope`. The quote returns `scope`, `price` (per unit expected and ceiling, overhead, total or units covered, unpriced, unmeasured) and the `done_check` the goal is held to. Returns a goal id to poll. Paid tool (quote-to-pay against the budget).",
4
28
  inputSchema: {
5
29
  type: "object",
6
30
  properties: {
@@ -43,6 +67,159 @@ export const computeTool = {
43
67
  },
44
68
  additionalProperties: false,
45
69
  },
70
+ done_check: {
71
+ description: "What done means, written before the goal runs and evaluated in code: the goal completes on its own when the check is met. Without one the goal waits in awaiting_confirmation for its owner to confirm. Give one shape.",
72
+ oneOf: [
73
+ {
74
+ type: "object",
75
+ description: "At least at_least tasks of a tool (of: tasks, needs tool), events of one type (of: events, needs event_type) or emails that reached a status (of: emails).",
76
+ properties: {
77
+ kind: { const: "count" },
78
+ of: { type: "string", enum: ["tasks", "events", "emails"] },
79
+ tool: KNOWN_GOAL_TOOL,
80
+ status: { type: "string", description: 'For tasks: "completed" or "failed"; for emails: "sent", "delivered" or "opened"' },
81
+ event_type: { type: "string", enum: ["email_reply", "sms_reply", "voice_completed", "task_completed", "task_failed"] },
82
+ at_least: { type: "integer", minimum: 1 },
83
+ },
84
+ required: ["kind", "of", "at_least"],
85
+ additionalProperties: false,
86
+ },
87
+ {
88
+ type: "object",
89
+ description: "The latest completed result of a tool, at a dot path (omit for the whole result), equals a value or is at least a number. Give exactly one of equals or at_least.",
90
+ properties: {
91
+ kind: { const: "tool_result" },
92
+ tool: KNOWN_GOAL_TOOL,
93
+ path: { type: "string", description: 'Dot path into the result, e.g. "result.status" (up to 8 keys)' },
94
+ equals: { type: ["string", "number", "boolean"] },
95
+ at_least: { type: "number" },
96
+ },
97
+ required: ["kind", "tool"],
98
+ // The API rejects a tool_result check with neither or both criteria; say so in the schema so a client fails before the quote.
99
+ oneOf: [{ required: ["equals"] }, { required: ["at_least"] }],
100
+ additionalProperties: false,
101
+ },
102
+ {
103
+ type: "object",
104
+ description: "No check can be written: the owner confirms completion.",
105
+ properties: { kind: { const: "owner" } },
106
+ required: ["kind"],
107
+ additionalProperties: false,
108
+ },
109
+ {
110
+ type: "object",
111
+ description: "Every case of the goal is in one of finished_states (a state in owner_required_for also needs an owner on the record); at least min_cases (default 1) must exist.",
112
+ properties: {
113
+ kind: { const: "cases" },
114
+ finished_states: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 20, description: 'Lowercase state tokens such as "merged"' },
115
+ owner_required_for: { type: "array", items: { type: "string" }, maxItems: 20, description: "States from finished_states that only count when the case record names an owner" },
116
+ min_cases: { type: "integer", minimum: 1 },
117
+ },
118
+ required: ["kind", "finished_states"],
119
+ additionalProperties: false,
120
+ },
121
+ {
122
+ type: "object",
123
+ description: "A named verifier accepts proof the agent submits (a document, photo, link or fields read from a document). The verifier needs an email or a phone number. With fields, a field the proof does not show, or one outside its bounds, is reported back without asking the verifier. One-off goals only.",
124
+ properties: {
125
+ kind: { const: "evidence" },
126
+ verifier: {
127
+ type: "object",
128
+ properties: {
129
+ name: { type: "string" },
130
+ email: { type: "string" },
131
+ phone: { type: "string", description: "E.164, such as +14155550100" },
132
+ timezone: { type: "string", description: 'IANA zone, e.g. "America/Chicago", for texts' },
133
+ },
134
+ required: ["name"],
135
+ additionalProperties: false,
136
+ },
137
+ accepts: { type: "string", description: "What proof is acceptable, in plain words; the verifier is shown it" },
138
+ fields: {
139
+ type: "array",
140
+ maxItems: 20,
141
+ items: {
142
+ type: "object",
143
+ properties: {
144
+ name: { type: "string" },
145
+ description: { type: "string" },
146
+ type: { type: "string", enum: ["string", "number", "boolean"] },
147
+ equals: { type: ["string", "number", "boolean"] },
148
+ one_of: { type: "array", items: { type: "string" } },
149
+ at_least: { type: "number" },
150
+ at_most: { type: "number" },
151
+ },
152
+ required: ["name", "description", "type"],
153
+ additionalProperties: false,
154
+ },
155
+ },
156
+ },
157
+ required: ["kind", "verifier", "accepts"],
158
+ additionalProperties: false,
159
+ },
160
+ ],
161
+ },
162
+ egress_allowlist: {
163
+ type: "array",
164
+ items: { type: "string" },
165
+ maxItems: 50,
166
+ description: "Hosts the goal's sandboxes may reach besides OneShot's API: hostnames, IPv4 addresses or IPv4 ranges. Setting it (even to []) limits every sandbox of the goal to OneShot's API and these hosts.",
167
+ },
168
+ scope: {
169
+ type: "object",
170
+ description: "Your reading of the goal, replacing the quote's and priced again in code. Omit it and the quote proposes one. A tool the goal cannot call is refused.",
171
+ properties: {
172
+ unit: {
173
+ type: "object",
174
+ description: "What one unit is and what starts it",
175
+ properties: {
176
+ name: { type: "string", description: 'One unit in your words, e.g. "a carrier invoice"' },
177
+ starts_on: { type: "string", enum: ["list", "event", "schedule", "once"], description: "A given list, an event in the world, a schedule, or the goal itself once" },
178
+ },
179
+ required: ["name", "starts_on"],
180
+ },
181
+ per_unit: {
182
+ type: "object",
183
+ description: "What one unit takes",
184
+ properties: {
185
+ uses: { type: "array", maxItems: 50, items: SCOPE_USE, description: "Tool uses inside one unit" },
186
+ step_allowance: { type: "integer", minimum: 0, description: "Model steps one unit may take beyond its tool uses" },
187
+ done: { type: "array", items: { type: "string" }, minItems: 1, description: 'Case states that finish a unit, e.g. ["paid"]; "unreached" is always among them' },
188
+ },
189
+ required: ["uses", "step_allowance", "done"],
190
+ },
191
+ overhead: {
192
+ type: "object",
193
+ description: "Tool uses outside any unit: setting up, reporting",
194
+ properties: {
195
+ uses: { type: "array", maxItems: 50, items: { type: "object", properties: { tool: KNOWN_GOAL_TOOL, qty: { type: "integer", minimum: 1 }, max_attempts: { type: "integer", minimum: 1 }, drivers: SCOPE_DRIVERS }, required: ["tool", "qty", "max_attempts"] } },
196
+ step_allowance: { type: "integer", minimum: 0 },
197
+ },
198
+ required: ["uses", "step_allowance"],
199
+ },
200
+ volume: {
201
+ type: "object",
202
+ description: 'How many units: {"kind": "list", "count": N} for a given list, {"kind": "open"} otherwise',
203
+ properties: { kind: { type: "string", enum: ["list", "open"] }, count: { type: "integer", minimum: 1 } },
204
+ required: ["kind"],
205
+ },
206
+ unpriced: {
207
+ type: "array",
208
+ maxItems: 50,
209
+ description: "What the price list cannot price: a connected system you pay for, or a tool quoted per call",
210
+ items: {
211
+ type: "object",
212
+ properties: {
213
+ name: { type: "string" },
214
+ reason: { type: "string", enum: ["connection", "quoted_tool"] },
215
+ assumption: { type: "string", description: "What remains unknown until a live quote, capped by the remaining goal budget" },
216
+ },
217
+ required: ["name", "reason"],
218
+ },
219
+ },
220
+ },
221
+ required: ["unit", "per_unit", "overhead", "volume", "unpriced"],
222
+ },
46
223
  },
47
224
  required: ["objective"],
48
225
  },
@@ -84,14 +261,15 @@ export const computeCancelTool = {
84
261
  };
85
262
  export const computeRespondTool = {
86
263
  name: "oneshot_compute_respond",
87
- description: "Respond to a human-in-the-loop task that a compute goal is waiting on (approve/reject or provide input), or send `dispute: true` without task_id to dispute a goal that ended impossible_unverified, with `response` as the reason; nothing is refunded. Free.",
264
+ description: "Respond to a human-in-the-loop task that a compute goal is waiting on (approve/reject or provide input), send `continue_without_tool: true` without task_id to release a goal awaiting a requested tool (with `response` as the message it resumes with), or send `dispute: true` without task_id to dispute a goal that ended impossible_unverified, with `response` as the reason; nothing is refunded. Free.",
88
265
  inputSchema: {
89
266
  type: "object",
90
267
  properties: {
91
268
  goal_id: { type: "string", description: "Compute goal id" },
92
- task_id: { type: "string", description: "Id of the task awaiting a response (not with dispute)" },
93
- response: { type: "string", description: "Free-text response for the task, or the reason for a dispute" },
269
+ task_id: { type: "string", description: "Id of the task awaiting a response (not with continue_without_tool or dispute)" },
270
+ response: { type: "string", description: "Free-text response for the task, the message an awaiting_tool goal resumes with, or the reason for a dispute" },
94
271
  approved: { type: "boolean", description: "Approve (true) or reject (false) an approval task" },
272
+ continue_without_tool: { type: "boolean", description: "Continue an awaiting_tool goal without the requested tool; must be true, without task_id" },
95
273
  dispute: { type: "boolean", description: "Dispute an impossible_unverified ending; must be true, without task_id" },
96
274
  },
97
275
  required: ["goal_id"],
@@ -99,7 +277,7 @@ export const computeRespondTool = {
99
277
  };
100
278
  export const computePauseTool = {
101
279
  name: "oneshot_compute_pause",
102
- description: "Pause a recurring compute goal. Its plan and progress are kept — the goal stops taking scheduled runs until resumed. Only recurring (scheduled) goals can be paused. Free.",
280
+ description: "Pause a compute goal. A recurring goal stops taking scheduled runs until resumed; any other goal keeps its sessions, cases and reserved budget and continues where it was when resumed. Free.",
103
281
  inputSchema: {
104
282
  type: "object",
105
283
  properties: {
@@ -111,7 +289,7 @@ export const computePauseTool = {
111
289
  };
112
290
  export const computeResumeTool = {
113
291
  name: "oneshot_compute_resume",
114
- description: "Resume a paused recurring compute goal. Picks up on the existing plan at the next scheduled run — nothing is lost. Free.",
292
+ description: "Resume a paused compute goal. A recurring goal picks up at its next scheduled run; any other goal continues now where it was. Nothing is lost. Free.",
115
293
  inputSchema: {
116
294
  type: "object",
117
295
  properties: {
@@ -133,7 +311,7 @@ export const computeFundTool = {
133
311
  },
134
312
  };
135
313
  export async function handleCompute(agent, args) {
136
- return agent.compute({
314
+ const options = {
137
315
  objective: args.objective,
138
316
  budget_usdc: args.budget_usdc,
139
317
  params: args.params,
@@ -142,7 +320,12 @@ export async function handleCompute(agent, args) {
142
320
  soul_service_slug: args.soul_service_slug,
143
321
  schedule: args.schedule,
144
322
  approvals: args.approvals,
145
- });
323
+ done_check: args.done_check,
324
+ egress_allowlist: args.egress_allowlist,
325
+ };
326
+ if (args.scope !== undefined)
327
+ options.scope = args.scope;
328
+ return agent.compute(options);
146
329
  }
147
330
  export async function handleComputeStatus(agent, args) {
148
331
  return agent.getComputeGoal(args.goal_id);
@@ -163,6 +346,12 @@ export async function handleComputeRespond(agent, args) {
163
346
  throw new Error("Send dispute: true, with an optional response, and without task_id");
164
347
  return agent.disputeComputeGoal(args.goal_id, { reason: args.response });
165
348
  }
349
+ // The owner continues a goal held for a requested tool without it (#3173); no task answers it either.
350
+ if (args.continue_without_tool !== undefined) {
351
+ if (args.continue_without_tool !== true || args.task_id !== undefined)
352
+ throw new Error("Send continue_without_tool: true, with an optional response, and without task_id");
353
+ return agent.continueComputeGoalWithoutTool(args.goal_id, { message: args.response });
354
+ }
166
355
  return agent.respondToComputeTask(args.goal_id, {
167
356
  task_id: args.task_id,
168
357
  response: args.response,
@@ -3,7 +3,7 @@ import type { OneShot } from "@oneshot-agent/sdk";
3
3
  /**
4
4
  * The agent's connections (#2403). Hand-written because `GET /v1/connections` is not a declared route: the
5
5
  * connected-system read tools (`oneshot_ledger_*`, `oneshot_crm_*`, `oneshot_shipping_*`, `oneshot_tax_*`,
6
- * `oneshot_procurement_*`) are generated from their routes and each tells the model to call this one first.
6
+ * `oneshot_procurement_*`, `oneshot_meetings_*`, `oneshot_field_service_*`) are generated from their routes and each tells the model to call this one first.
7
7
  * Connecting and revoking stay in the console.
8
8
  */
9
9
  export declare const connectionsListTool: Tool;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The agent's connections (#2403). Hand-written because `GET /v1/connections` is not a declared route: the
3
3
  * connected-system read tools (`oneshot_ledger_*`, `oneshot_crm_*`, `oneshot_shipping_*`, `oneshot_tax_*`,
4
- * `oneshot_procurement_*`) are generated from their routes and each tells the model to call this one first.
4
+ * `oneshot_procurement_*`, `oneshot_meetings_*`, `oneshot_field_service_*`) are generated from their routes and each tells the model to call this one first.
5
5
  * Connecting and revoking stay in the console.
6
6
  */
7
7
  export const connectionsListTool = {
@@ -1,6 +1,6 @@
1
1
  export const discoverToolsTool = {
2
2
  name: 'oneshot_discover_tools',
3
- description: 'Find OneShot tools by task or category. Free. Returns public names, purposes and required inputs. With no query or category, lists categories. Category pages contain up to five tools; pass nextCursor with the same query and category.',
3
+ description: 'Find OneShot tools by task or category. Free. Returns public names, purposes and required inputs; a lookup that is not in your tool list runs with oneshot_lookup. With no query or category, lists categories. Category pages contain up to five tools; pass nextCursor with the same query and category.',
4
4
  inputSchema: {
5
5
  type: 'object',
6
6
  properties: {
@@ -1,6 +1,6 @@
1
1
  export const listDomainsTool = {
2
2
  name: "oneshot_list_domains",
3
- description: "List the agent's sending domains. Each has default_from (canonical default sender, e.g. agent@domain — billed like any address on its first send), mailbox_mode (relay|mailbox), addresses[] (mailboxes already provisioned — {address, status, warmup_state}; empty means none yet), pool_status (rotation eligibility: active|paused|removed), warmup_state (reputation health: warming|warmed|degraded), pause_reason (warming|low_reputation|manual|null), warmup_score, and daily send limits. mailbox_mode is a property of the domain, fixed when it was set up: on a 'mailbox' domain EVERY address not already active in addresses[] bills the one-time mailbox_provisioning_fee on its first send, the canonical default_from included. 'relay' domains never charge it. A domain can be paused AND warming — reputation pauses auto-recover once warmed; manual pauses need a resume. Free.",
3
+ description: "List the agent's sending domains. Each has default_from (the main address, e.g. agent@domain; rotation and goals send from it), mailbox_mode (relay|mailbox), addresses[] (the addresses the domain sends and receives at: {address, status, warmup_state}), pool_status (rotation eligibility: active|paused|removed), warmup_state (reputation health: warming|warmed|degraded|not_warmed), pause_reason (warming|low_reputation|manual|null), warmup_score, and daily send limits. mailbox_mode is fixed when the domain is bought. relay: addresses send and receive with no hosted inbox; never warmed (not_warmed), they rotate at once on a daily limit that rises with the domain's age. mailbox: each address is a hosted inbox, warmed before it rotates. The owner adds addresses with the domain purchase; a send never creates one. A domain can be paused AND warming: reputation pauses auto-recover once warmed; manual pauses need a resume. Free.",
4
4
  inputSchema: { type: "object", properties: {} },
5
5
  };
6
6
  export const pauseDomainTool = {
@@ -1,7 +1,7 @@
1
1
  export const emailTool = {
2
2
  name: "oneshot_email",
3
3
  description: "Send an email. Supports HTML body, attachments, and threaded replies (set reply_to_email_id to an inbound email id from oneshot_inbox_list/oneshot_inbox_get, or one this agent sent). Paid tool — quote provided before payment.\n\n" +
4
- "Sending requires a domain the agent owns. A fresh account has none — call oneshot_list_domains first to see what's already owned. If there is no usable domain, pass from_domain with a new domain to register it. A call with a new from_domain only quotes — nothing is charged — and returns status 'confirmation_required' with the exact cost (domain registration plus fees) and a quote_id. Tell the user that exact computed number, never a guessed or approximate one, and only once they say to proceed, call this again with the same arguments and confirm_new_domain set to that quote_id: that registers the domain, charges exactly the quoted total, and sends the email in one call. The workspace balance must cover it, or the call fails with insufficient_credits.",
4
+ "Sending requires an owned, verified domain, and a domain sends only from its own addresses. Call oneshot_list_domains first. A send never sets up a domain or an address: a new from_domain, or a from_mailbox the domain does not have, returns status confirmation_required with a separate setup quote, and no agent domain at all returns setup_required with requirements. Nothing is charged or sent. Show setup.total_cost and setup.billing to the owner, who buys through POST /v1/domains/buy or the account console. Models and goals cannot buy setup. Send again once setup is done.",
5
5
  inputSchema: {
6
6
  type: "object",
7
7
  properties: {
@@ -23,21 +23,16 @@ export const emailTool = {
23
23
  },
24
24
  from_domain: {
25
25
  type: "string",
26
- description: "Domain to send from. If you already own it, pass it to pin that domain. If you don't, pass a new domain here to register it — a call with a new from_domain only quotes (status 'confirmation_required' with a quote_id, nothing charged); repeat the call with confirm_new_domain set to that quote_id to register it, charge exactly the quoted registration + service fee (and a mailbox fee if mailbox_mode is 'mailbox'), and send. Only confirm once the user has seen that amount and asked to proceed. Omit both from_domain and from_mailbox to rotate across your warmed domains — the chosen address comes back on the result. Call oneshot_list_domains first to see what you already own.",
26
+ description: "Pin an owned sending domain, or choose a new name to quote separate account setup. The owner must buy setup and wait for verification before sending. Omit both from_domain and from_mailbox to rotate across eligible owned domains, each sending from its main address.",
27
27
  },
28
28
  from_mailbox: {
29
29
  type: "string",
30
- description: "Sender mailbox / local-part (optional, defaults to 'agent', i.e. agent@from_domain). Requires from_domain to be set — pinning from_mailbox alone is rejected, since there is no domain to attach it to. You can use any local-part — a new address is auto-provisioned on first use (starts unwarmed, warms over time). The FIRST send from any address — including the default 'agent@' — adds a one-time mailbox_provisioning_fee to the quote; it's free only once that address is already provisioned (active). See the domains list for each domain's default_from and existing addresses.",
30
+ description: "Sender local-part on from_domain (optional, defaults to the domain's main address, its default_from). Requires from_domain. Must be one of the domain's addresses (see oneshot_list_domains); another returns the setup quote that adds it, and no email is sent.",
31
31
  },
32
32
  from_name: {
33
33
  type: "string",
34
34
  description: "Display name shown to the recipient (optional), e.g. 'Jane Doe' renders as 'Jane Doe <jane@domain>'",
35
35
  },
36
- mailbox_mode: {
37
- type: "string",
38
- enum: ["relay", "mailbox"],
39
- description: "How this domain sends (optional). 'relay' = no dedicated mailbox, no mailbox fee. 'mailbox' = a real dedicated mailbox per address (better deliverability + per-address warmup); each new address adds a one-time mailbox_provisioning_fee to the quote. Only honored while a domain is unprovisioned (brand new, or owned with provisioning_status 'unprovisioned') — an already-provisioned domain keeps the mode it was created with, which means OMITTING this does not make a send relay and does not avoid the fee. On a domain already in mailbox mode every address that isn't active yet bills the fee on its first send, the default agent@ included; from_mailbox does not change that. Use oneshot_list_domains to see each domain's mailbox_mode before quoting.",
40
- },
41
36
  attachments: {
42
37
  type: "array",
43
38
  items: {
@@ -55,10 +50,6 @@ export const emailTool = {
55
50
  type: "string",
56
51
  description: "Only when retrying a call the action policy held: the approval_id from the approval_required error, once a human approved it. The call then runs once.",
57
52
  },
58
- confirm_new_domain: {
59
- type: "string",
60
- description: "The quote_id from a 'confirmation_required' result, and nothing else. A call with a from_domain you don't own yet always stops before charging and returns that result with the exact total. Only after showing that total to the user and getting their go-ahead, call this again with the same arguments and confirm_new_domain set to the quote_id: the domain is registered, exactly that quote's total is charged, and the email is sent in that one call. Passing true never charges; it only quotes again. A wrong, expired or already-used quote id is rejected by the server without charge: drop it and call again without confirm_new_domain for a fresh quote.",
61
- },
62
53
  },
63
54
  required: ["body"],
64
55
  },
@@ -72,10 +63,7 @@ export async function handleEmail(agent, args) {
72
63
  from_domain: args.from_domain,
73
64
  from_mailbox: args.from_mailbox,
74
65
  from_name: args.from_name,
75
- mailbox_mode: args.mailbox_mode,
76
66
  attachments: args.attachments,
77
67
  approvalId: args.approval_id,
78
- // Only the quote id confirms; `true` or anything else is not a confirmation.
79
- confirmNewDomain: typeof args.confirm_new_domain === "string" ? args.confirm_new_domain : undefined,
80
68
  });
81
69
  }
@@ -1,5 +1,6 @@
1
1
  import { UNTRUSTED_TOOLS } from './untrusted-tools.generated.js';
2
2
  import { discoverToolsTool, handleDiscoverTools } from './discover.js';
3
+ import { handleLookupCall, lookupCallTool, lookupTarget } from './lookup-call.js';
3
4
  import { emailTool, handleEmail } from "./email.js";
4
5
  import { voiceTool, handleVoice } from "./voice.js";
5
6
  import { voiceNumberProvisionTool, voiceNumbersTool, voiceInboundSetTool, voiceInboundDisableTool, voiceInboundCallsTool, handleVoiceNumberProvision, handleVoiceNumbers, handleVoiceInboundSet, handleVoiceInboundDisable, handleVoiceInboundCalls, } from "./voice-inbound.js";
@@ -34,6 +35,7 @@ import { connectionsListTool, handleConnectionsList } from "./connections.js";
34
35
  import { spendBreakdownTool, rocsTool, rocsByGoalTool, receiptsListTool, tagReceiptValueTool, handleSpendBreakdown, handleRocs, handleRocsByGoal, handleReceiptsList, handleTagReceiptValue } from "./analytics.js";
35
36
  const toolDefinitions = [
36
37
  discoverToolsTool,
38
+ lookupCallTool,
37
39
  emailTool,
38
40
  voiceTool,
39
41
  smsTool,
@@ -130,6 +132,7 @@ export const tools = toolDefinitions.map((tool) => {
130
132
  });
131
133
  const handlers = {
132
134
  oneshot_discover_tools: handleDiscoverTools,
135
+ oneshot_lookup: handleLookupCall,
133
136
  "oneshot_email": handleEmail,
134
137
  "oneshot_voice": handleVoice,
135
138
  "oneshot_sms": handleSms,
@@ -227,7 +230,9 @@ export async function handleToolCall(agent, toolName, args) {
227
230
  isError: true,
228
231
  };
229
232
  }
230
- const untrusted = UNTRUSTED_TOOLS.has(toolName.replace(/^oneshot_/, ""));
233
+ // oneshot_lookup returns what the lookup it runs returns, so it is marked by that lookup.
234
+ const marked = toolName === "oneshot_lookup" && args.input !== undefined ? lookupTarget(args) ?? toolName : toolName;
235
+ const untrusted = UNTRUSTED_TOOLS.has(marked.replace(/^oneshot_/, ""));
231
236
  try {
232
237
  const data = await handler(agent, args);
233
238
  const acknowledgement = data && typeof data === "object" && "request_id" in data && "status" in data
@@ -282,6 +287,12 @@ export function structuredToolError(error) {
282
287
  };
283
288
  case "PaymentError":
284
289
  return { error: "payment_failed", message: e.message, reason: e.reason };
290
+ case "SettlementAmbiguousError":
291
+ return {
292
+ error: "settlement_ambiguous",
293
+ message: e.message,
294
+ ...(typeof e.settlementId === "string" ? { settlement_id: e.settlementId } : {}),
295
+ };
285
296
  case "ToolError": {
286
297
  // Every 4xx the SDK doesn't recognize as one of the typed errors above
287
298
  // (budget/credits/payment) lands here as a plain ToolError, and its