Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## Unreleased

- Managed prospecting agent coordination and contact qualification now use customer LLM credentials, including when contact extraction is native. Missing keys and provider errors do not fall back to platform credentials.
- SDK: prospecting runs gain `chat_closed`, set once a chat is closed for repeated off-topic use; every later message then gets a fixed reply instead of a model call. A run with no approved plan, or an approved run paused waiting on a question, is cancelled with the new `stop_reason` value `"misuse"` when its chat closes; an approved queued or running run keeps working and its saved lists still fill. Starting a run past an organization's daily new-conversation limit now returns a 403 (`PlanAccessError`) with a message naming the limit.

- CLI: add prospecting plan approval, recent-run lists, chat messages, and event/message cursors. Omitted quantity flags preserve brief inference; larger target and automatic work limits match the API. `wait` returns when a plan needs approval.
- SDK: add sync/async prospecting start/get/list/approve/message/cancel/wait, with typed chat, progress, saved-query, and recent-run responses. Starts and messages require idempotency keys; approval requires the reviewed plan version. `wait` returns on proposed plans, needs-input, and terminal outcomes.
- SDK: prospecting runs gain `saved_query_ids`, every saved contact list for the run in order (first entry is `saved_query_id`). Large results are now split across several lists instead of being cut off at 50 MiB; parts are final once the run reaches a terminal status.
- SDK: prospecting checkpoints. `ProspectingBrief.checkpoints` (`"ask"` or `"auto"`, default `"auto"`, the API's default) and `ProspectingApproveRequest.checkpoints` (`None` keeps the brief's mode). In `"ask"` mode a run pauses with `status="needs_input"` and a `stop_reason` in the new `CHECKPOINT_STOP_REASONS` (`pilot`, `tail_quality`, `short`, `target_reached`, exported from `discolike`); the latest `kind="question"` message carries `data.suggested_replies` and, at a pilot, `data.sample`. Answer with `message()` using a reply's exact text and `wait()` again; `wait()` already returns on `needs_input`. `"auto"` never pauses: a poor pilot is sharpened once and the run continues with a notice. It only stops with the new `stop_reason` `"pilot_failed"` (and the new `ProspectingRun.pilot_sample` lists the checked companies) if the re-pilot fit is still under 20%; if sharpening itself fails, the run continues on the original criteria. Finishing at a checkpoint stops with `"user_finished"`.
- CLI: `prospecting start` and `prospecting approve` send `checkpoints="ask"` by default, matching the web chat; `--auto` sends `"auto"`. `prospecting wait` asks at a checkpoint on a terminal (question, sample companies, numbered suggested replies or free text), posts the answer and keeps waiting. Without a terminal, or with the new `--no-input`, it prints the run on stdout, a `needs_input` envelope with the question and `suggested_replies` on stderr, and exits with the new exit code 7. Other `needs_input` pauses still exit 0.
- SDK (note for maintainers): `ProspectingBrief.checkpoints` is hidden from the platform's OpenAPI schema, so `scripts/gen_requests.py` pins it through `PROPERTY_OVERRIDES` and `scripts/check_contract.py` skips it via `HIDDEN_REQUEST_FIELDS`. `ProspectingApproveRequest.checkpoints` is in `PENDING_PROPERTIES` until the platform deploys it; after that, regenerating moves the class within `_generated/requests.py`, which `--check` reports as a diff until you regenerate.
- SDK/CLI: `prospecting.list` / `ProspectingListParams` / `prospecting list --before` gain `before` (a run ID) for keyset paging past a full page of runs, ordered by `created_at` then `run_id` descending. An unknown or other-organization run ID returns an empty page.

- SDK/CLI: `discogen.process` / `discogen.process_personas` with `typed_columns=True` now raise a 400 `ValidationError` at submit when the account has no TypeSafe integration, instead of returning a job whose typed cells all read `Error: No TypeSafe integration is configured`. No SDK code change; the server rejects earlier.

## 0.4.1 (2026-09-23)

- CLI: fix `ImportError: cannot import name 'Abort' from 'typer._click.exceptions'` on every command in a fresh 0.4.0 install. Typer 0.27 moved `Abort`; the CLI now imports the public `typer.Abort` and requires `typer>=0.26.1,<0.28`, since it still relies on Typer's vendored Click for its error envelope and help formatting.
Expand Down
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ Top-level commands: `discover`, `count`, `match`, `extract`, `validate-icp`, `ap
| 4 | Rate limited |
| 5 | Network error |
| 6 | Not found |
| 7 | Needs input: `prospecting wait` reached a checkpoint with no terminal to ask (or `--no-input`) |

## What's in the box

Expand Down Expand Up @@ -421,3 +422,46 @@ Committed request models track the dev spec (`--spec-url https://api.dev.discoli
## License

[MIT](LICENSE)


### Managed prospecting

REST starts in `drafting`, then waits at `proposed` for approval. Review the plan and approve its exact version before research starts.

```python
from discolike.requests import (
ProspectingApproveRequest, ProspectingBrief, ProspectingGetParams,
ProspectingListParams, ProspectingMessageRequest,
)

run = client.prospecting.start(
ProspectingBrief(brief="Find 100 US logistics companies and 3 operations directors each"),
idempotency_key="logistics-search-2026-09-26",
)
run = client.prospecting.wait(run.run_id)
print(run.status, run.plan, run.messages) # Review before approving.
# After reviewing a proposed plan:
# client.prospecting.approve(run.run_id, ProspectingApproveRequest(plan_version=run.plan_version))
# run = client.prospecting.wait(run.run_id)

recent = client.prospecting.list(ProspectingListParams(limit=20))
message = client.prospecting.message(
run.run_id, ProspectingMessageRequest(text="Make it 250 companies"),
idempotency_key="logistics-target-edit-1",
)
page = client.prospecting.get(
run.run_id,
ProspectingGetParams(offset=0, limit=100, events_after=run.next_event_seq, messages_after=run.next_message_seq),
)
# client.prospecting.cancel(run.run_id)
```

The async client exposes the same methods with `await`. Starts and messages require separate idempotency keys; reuse each key when retrying that operation. Approving an already approved version is safe. A stale plan version is rejected: fetch the current plan and review it again.

`wait()` returns on `proposed`, `needs_input`, `completed`, `failed`, or `cancelled`. Inspect `status`, `stop_reason`, and `error`; completion does not guarantee the target was reached. A local timeout stops polling only. Partial results remain available. Use `get()` with event and message cursors to receive the agent's reply after sending a message; `reply_pending` indicates a pending reply. A `needs_input` question can be answered with `message()`.

Checkpoints: `ProspectingBrief(checkpoints=...)` picks how a run handles its decision points. `"auto"`, the API default, never pauses. A run of 500+ target companies from a brief (not a domain list) checks its first companies before looking up contacts; under 80% fit, auto sharpens the criteria once and checks again, then stops with `stop_reason="pilot_failed"` and `pilot_sample` holding the checked companies (`domain`, `name`, `company_fit`, `reason`); start a new run with a sharper brief. A search drifting off target is dropped, a run short of candidates finishes as `candidates_exhausted`, and a met target finishes the run. `"ask"` pauses with `status="needs_input"` and a `stop_reason` in `CHECKPOINT_STOP_REASONS` (`pilot`, `tail_quality`, `short`, `target_reached`). The latest `kind="question"` message carries `data.suggested_replies` and, at a pilot, `data.sample`; answer with `message()` using a suggested reply's exact text (or free-text steering), then `wait()` again. Choosing to finish at a checkpoint ends the run with `stop_reason="user_finished"`. `ProspectingApproveRequest(checkpoints=...)` overrides the brief's mode at approval; `None` keeps it. `checkpoints` on the brief is not in the published OpenAPI schema; the SDK sends it anyway.

Initial planning extracts company counts and contacts per company from the brief. Omitted settings keep that inference available, falling back to 25 companies and 2 contacts per company. Explicit settings, including explicit defaults, override the text. Targets support 1–10,000 companies and 1–5 contacts per company. Candidate and action caps default to automatic (`0`); explicit maxima are 100,000 candidates and 10,000 actions. Result pages support up to 500 rows; recent-run lists support up to 50. Approved runs expose a stable `saved_query_id` for saved results.

Existing processing charges and configured BYOK/BYOS integrations apply. Wizard interpretation, segmentation, and prompt preparation use platform credentials. Agent coordination and independent contact qualification use your contact LLM integration; native contacts use your validation LLM integration or organization default, so this workflow requires a customer LLM even with native extraction. Missing keys and provider errors never fall back to platform keys. Limits bound work, not provider dollar spend. Email finder outcomes are exposed; raw email verification is not a public API.
22 changes: 21 additions & 1 deletion packages/discolike-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ discolike company data stripe.com
discolike extract https://stripe.com/enterprise
```

Top-level commands: `discover`, `count`, `match`, `extract`, `validate-icp`, `append`, `segment` — plus `auth`, `bulk`, `company`, `contacts`, `discogen`, `queries`, `account`, `search-providers`, and `llm-providers` command groups.
Top-level commands: `discover`, `count`, `match`, `extract`, `validate-icp`, `append`, `segment` — plus `auth`, `bulk`, `company`, `contacts`, `discogen`, `prospecting`, `queries`, `account`, `search-providers`, and `llm-providers` command groups.

### Volume pulls

Expand All @@ -49,6 +49,25 @@ discolike bulk contacts --domains-file companies.csv --per-company 10 --summary

`companies` saves each page as an exclusion list (`<run-name>-round-N`) and excludes it from the next page; rerunning with the same `--out` resumes from the CSV. `contacts` slices the domain list at `10000 / per-company` domains per call and records finished slices in `<out>.checkpoint`. Both keep one call in flight under `--rate-limit` (default 10/min, the Pro rate on `/discover` and `/contacts`), retry on 429/5xx, and print a JSON summary at the end. Filters come from `--params-file`, `--param` and the common flags; the paging fields are managed for you.

### Managed prospecting

```bash
discolike prospecting start --brief "Find 100 US logistics companies and 3 operations directors each" --idempotency-key logistics-1
discolike prospecting wait RUN_ID
# Review the proposed plan, then approve the exact version you saw:
discolike prospecting approve RUN_ID --plan-version 1
discolike prospecting list --limit 20
discolike prospecting message RUN_ID --text "Make it 250 companies" --idempotency-key logistics-edit-1
discolike prospecting status RUN_ID --events-after 12 --messages-after 8 --limit 100
discolike prospecting cancel RUN_ID
```

`wait` returns on `proposed`, `needs_input`, `completed`, `failed`, or `cancelled`. A timeout stops polling only. Inspect status and stop reason; completion does not guarantee full coverage. Message replies arrive through `status --messages-after`; follow `next_message_seq` and `reply_pending`.

`start` and `approve` default to pausing at checkpoints, like the web chat: a pilot check on large lists (`pilot`), a search drifting off target (`tail_quality`), candidates running out short of the target (`short`), and the target being met (`target_reached`). Pass `--auto` to never pause; a poor pilot is then sharpened once and the run continues with a notice, stopping with `pilot_failed` only if the re-pilot fit is still under 20%. If sharpening itself fails, the run continues on the original criteria. On a terminal, `wait` shows the question, any sample companies and numbered replies at a checkpoint, sends your pick or your own text, and keeps waiting. Without a terminal, or with `--no-input`, it prints the run on stdout, a `needs_input` envelope (`message`, `stop_reason`, `suggested_replies`, `sample`) on stderr, and exits 7; answer with `prospecting message --text "<reply>"` and run `wait` again.

Omit `--target-companies` and `--contacts-per-company` to infer counts from the brief (fallback 25 and 2). Explicit values override the text. `--max-candidates` and `--max-actions` are automatic when omitted or `0`; their maxima are 100,000 and 10,000. Targets allow up to 10,000 companies, status pages up to 500 rows, and lists up to 50 runs. Work caps do not cap provider charges.

### Conventions

- Results print as JSON to stdout; errors print as JSON (`error`, `message`, `status_code`) to stderr.
Expand All @@ -67,6 +86,7 @@ discolike bulk contacts --domains-file companies.csv --per-company 10 --summary
| 4 | Rate limited |
| 5 | Network error |
| 6 | Not found |
| 7 | Needs input: `prospecting wait` reached a checkpoint with no terminal to ask (or `--no-input`) |

## Links

Expand Down
3 changes: 3 additions & 0 deletions packages/discolike-cli/src/discolike_cli/_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ def format_help(self, ctx: Context, formatter: HelpFormatter) -> None:
4 rate_limited HTTP 429; wait "retry_after" seconds, then retry
5 network_error could not reach the API
6 not_found HTTP 404
7 needs_input `prospecting wait` stopped at a checkpoint with no terminal to ask
(or --no-input): the run is on stdout, the question and
"suggested_replies" in the stderr envelope

Environment:
{ENV_API_KEY} API key; overrides the config file written by `discolike auth login`.
Expand Down
1 change: 1 addition & 0 deletions packages/discolike-cli/src/discolike_cli/_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
NotFoundError: 6,
}
DEFAULT_EXIT_CODE = 1
NEEDS_INPUT_EXIT_CODE = 7

# Stable, snake_case error codes for agents and scripts to branch on. The class
# name in `error` is kept for backwards compatibility; `code` is the contract.
Expand Down
2 changes: 2 additions & 0 deletions packages/discolike-cli/src/discolike_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from discolike_cli import email
from discolike_cli import enrich
from discolike_cli import match
from discolike_cli import prospecting
from discolike_cli import providers
from discolike_cli import queries
from discolike_cli import signup
Expand Down Expand Up @@ -85,6 +86,7 @@ def get_client(ctx: typer.Context) -> Discolike:
app.add_typer(discogen.app, name="discogen")
app.add_typer(email.app, name="email")
app.add_typer(queries.app, name="queries")
app.add_typer(prospecting.app, name="prospecting")
app.add_typer(account.app, name="account")
app.add_typer(providers.search_providers_app, name="search-providers")
app.add_typer(providers.llm_providers_app, name="llm-providers")
Expand Down
Loading
Loading