corethink 1.0.0__py3-none-any.whl
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.
- corethink/__init__.py +17 -0
- corethink/__main__.py +3 -0
- corethink/_version.py +3 -0
- corethink/account.py +470 -0
- corethink/cli.py +1058 -0
- corethink/client.py +253 -0
- corethink/commands/__init__.py +0 -0
- corethink/commands/admin.py +703 -0
- corethink/commands/api.py +59 -0
- corethink/commands/archive.py +433 -0
- corethink/commands/auth.py +375 -0
- corethink/commands/billing.py +314 -0
- corethink/commands/capture.py +564 -0
- corethink/commands/connection.py +1266 -0
- corethink/commands/endpoint.py +1124 -0
- corethink/commands/env.py +1966 -0
- corethink/commands/hub.py +269 -0
- corethink/commands/library.py +347 -0
- corethink/commands/models.py +258 -0
- corethink/commands/train.py +2655 -0
- corethink/commands/workspace.py +622 -0
- corethink/config.py +181 -0
- corethink/context.py +46 -0
- corethink/env_template.py +117 -0
- corethink/environment_http_store.py +96 -0
- corethink/experiment_cli.py +2187 -0
- corethink/hosted_profiles.py +44 -0
- corethink/hosted_rl.py +688 -0
- corethink/hosted_rl_archive.py +74 -0
- corethink/hosted_rl_attribution.py +44 -0
- corethink/hosted_rl_billing.py +103 -0
- corethink/hosted_rl_state.py +182 -0
- corethink/jobs_cli.py +839 -0
- corethink/main.py +204 -0
- corethink/output.py +201 -0
- corethink/py.typed +0 -0
- corethink/rl_cli.py +997 -0
- corethink/rl_inspect.py +521 -0
- corethink/rl_playground.py +129 -0
- corethink/rl_reassessment.py +214 -0
- corethink/rl_settings.py +43 -0
- corethink/sdk.py +331 -0
- corethink/settings.py +162 -0
- corethink-1.0.0.dist-info/METADATA +120 -0
- corethink-1.0.0.dist-info/RECORD +48 -0
- corethink-1.0.0.dist-info/WHEEL +5 -0
- corethink-1.0.0.dist-info/entry_points.txt +2 -0
- corethink-1.0.0.dist-info/top_level.txt +1 -0
corethink/cli.py
ADDED
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
"""Thin CoreThink command-line front door."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from dataclasses import replace
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from corethink_evals import (
|
|
15
|
+
DEFAULT_SYSTEM_INSTRUCTIONS,
|
|
16
|
+
ArtifactReader,
|
|
17
|
+
EvaluationRunner,
|
|
18
|
+
NetworkMode,
|
|
19
|
+
OpenAICompatibleModelClient,
|
|
20
|
+
ProgramSource,
|
|
21
|
+
ProgramSpec,
|
|
22
|
+
PullPolicy,
|
|
23
|
+
RunConfig,
|
|
24
|
+
RuntimeResources,
|
|
25
|
+
RuntimeSelection,
|
|
26
|
+
TaskStatus,
|
|
27
|
+
initialize_program_definition,
|
|
28
|
+
load_program_definition,
|
|
29
|
+
load_taskset,
|
|
30
|
+
)
|
|
31
|
+
from corethink_evals.audit import RequestCorrelation, build_audit_bundle, write_audit_bundle
|
|
32
|
+
from corethink_evals.program_budget import DIAL_FIELDS, ReasoningEffort, resolve_effort
|
|
33
|
+
from corethink_experiments.errors import ExperimentError
|
|
34
|
+
|
|
35
|
+
from corethink import experiment_cli, jobs_cli
|
|
36
|
+
from corethink.account import AccountError
|
|
37
|
+
from corethink.config import ConfigError
|
|
38
|
+
from corethink.jobs_cli import JobsError
|
|
39
|
+
|
|
40
|
+
DEFAULT_BASE_URL = "https://nre-kernel-xfsavzevuq-uc.a.run.app/v1"
|
|
41
|
+
DEFAULT_MODEL = "corethink/nre-kernel"
|
|
42
|
+
DEFAULT_PROGRAM_ID = "corethink/local-eval"
|
|
43
|
+
DEFAULT_PROGRAM_VERSION = "0.1.0"
|
|
44
|
+
DEFAULT_MODEL_API_KEY_ENV = "CORETHINK_MODEL_API_KEY"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
#: Named endpoints, so the common case is a word rather than three flags.
|
|
48
|
+
#: Deliberately small and explicit: a preset that guesses a base URL from a model
|
|
49
|
+
#: name would send a customer's prompts somewhere they did not choose.
|
|
50
|
+
AGENT_PRESETS: dict[str, dict[str, str]] = {
|
|
51
|
+
"corethink": {
|
|
52
|
+
"base_url": DEFAULT_BASE_URL,
|
|
53
|
+
"model": DEFAULT_MODEL,
|
|
54
|
+
"description": "the public CoreThink Reasoner; needs no key",
|
|
55
|
+
},
|
|
56
|
+
"openrouter": {
|
|
57
|
+
"base_url": "https://openrouter.ai/api/v1",
|
|
58
|
+
# Match Workbench's hosted evaluation default. Explicit --model wins;
|
|
59
|
+
# the paid preset never silently falls back to a different/free model.
|
|
60
|
+
"model": "deepseek/deepseek-v4.1-flash",
|
|
61
|
+
"api_key_env": "OPENROUTER_API_KEY",
|
|
62
|
+
"description": "paid DeepSeek V4.1 Flash via OpenRouter; requires API key and credits",
|
|
63
|
+
},
|
|
64
|
+
"fake": {
|
|
65
|
+
"adapter": "fake",
|
|
66
|
+
"description": "the taskset's own build_fake_client; no key, no network",
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
72
|
+
parser = argparse.ArgumentParser(
|
|
73
|
+
prog="corethink",
|
|
74
|
+
description="Evaluate versioned agent programs with replayable stateful traces.",
|
|
75
|
+
)
|
|
76
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
77
|
+
|
|
78
|
+
evaluate = commands.add_parser("eval", help="run a local taskset evaluation")
|
|
79
|
+
evaluate.add_argument("taskset", help="taskset.py file or directory containing it")
|
|
80
|
+
evaluate.add_argument("--adapter", choices=("live", "fake"), default="live")
|
|
81
|
+
# Off by default. Requesting logprobs from an endpoint that does not support
|
|
82
|
+
# them can be rejected outright, so an evaluation that never wanted them must
|
|
83
|
+
# not start failing. Deliberately not a program-config key: config feeds the
|
|
84
|
+
# program hash, so capture there would make the same program with capture on a
|
|
85
|
+
# different program, and its RL data incomparable with its own baseline.
|
|
86
|
+
evaluate.add_argument(
|
|
87
|
+
"--temperature",
|
|
88
|
+
type=float,
|
|
89
|
+
default=0.0,
|
|
90
|
+
help="sampling temperature. 0 is deterministic, which makes every sample of "
|
|
91
|
+
"a prompt identical — and a GRPO group of identical answers has no gradient "
|
|
92
|
+
"signal, so collecting RL data needs this above 0.",
|
|
93
|
+
)
|
|
94
|
+
evaluate.add_argument("--top-p", type=float, default=None)
|
|
95
|
+
evaluate.add_argument(
|
|
96
|
+
"--seed",
|
|
97
|
+
type=int,
|
|
98
|
+
default=None,
|
|
99
|
+
help="reproduce one rollout exactly. Not for building groups: one seed makes "
|
|
100
|
+
"k samples identical, which is the thing a group must not be.",
|
|
101
|
+
)
|
|
102
|
+
evaluate.add_argument(
|
|
103
|
+
"--capture-logprobs",
|
|
104
|
+
action="store_true",
|
|
105
|
+
help="record per-token log-probabilities, which a policy gradient needs "
|
|
106
|
+
"and supervised fine-tuning does not",
|
|
107
|
+
)
|
|
108
|
+
# No parser default: with one, an explicit flag and the fallback are
|
|
109
|
+
# indistinguishable at resolution time, and a stored base_url could never
|
|
110
|
+
# take effect. The default is applied last, in `_resolved`.
|
|
111
|
+
evaluate.add_argument("--base-url")
|
|
112
|
+
evaluate.add_argument("--model")
|
|
113
|
+
evaluate.add_argument(
|
|
114
|
+
"--model-api-key-env",
|
|
115
|
+
default=DEFAULT_MODEL_API_KEY_ENV,
|
|
116
|
+
help="environment variable holding the model API key for --adapter live; "
|
|
117
|
+
"never passed on argv. Unset means the endpoint is called unauthenticated, "
|
|
118
|
+
"which is correct for the public CoreThink Reasoner and wrong for anything else.",
|
|
119
|
+
)
|
|
120
|
+
evaluate.add_argument(
|
|
121
|
+
"agent",
|
|
122
|
+
nargs="?",
|
|
123
|
+
help=(
|
|
124
|
+
"a named endpoint instead of --adapter/--base-url/--model: "
|
|
125
|
+
+ ", ".join(sorted(AGENT_PRESETS))
|
|
126
|
+
+ ". Explicit flags still win over the preset."
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
evaluate.add_argument("--out", default=".corethink", help="artifact root directory")
|
|
130
|
+
evaluate.add_argument(
|
|
131
|
+
"--group",
|
|
132
|
+
type=_positive_int,
|
|
133
|
+
default=1,
|
|
134
|
+
metavar="K",
|
|
135
|
+
help=(
|
|
136
|
+
"sample the whole taskset K times instead of once. A group-relative "
|
|
137
|
+
"baseline compares a completion against its own prompt's other "
|
|
138
|
+
"attempts, so K>1 is what produces an advantage at all — one attempt "
|
|
139
|
+
"per prompt has nothing to be relative to. Needs --temperature above "
|
|
140
|
+
"0, and is refused without it."
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
evaluate.add_argument("--limit", type=_positive_int)
|
|
144
|
+
evaluate.add_argument("--program", help="program.json file or directory containing it")
|
|
145
|
+
evaluate.add_argument("--program-id")
|
|
146
|
+
evaluate.add_argument("--program-version")
|
|
147
|
+
evaluate.add_argument("--system-instructions")
|
|
148
|
+
evaluate.add_argument("--max-model-calls", type=_positive_int)
|
|
149
|
+
evaluate.add_argument("--max-tool-calls", type=_positive_int)
|
|
150
|
+
evaluate.add_argument("--task-timeout", type=_positive_float)
|
|
151
|
+
evaluate.add_argument("--model-timeout", type=_positive_float)
|
|
152
|
+
evaluate.add_argument("--tool-timeout", type=_positive_float)
|
|
153
|
+
evaluate.add_argument("--retries", type=_nonnegative_int)
|
|
154
|
+
evaluate.add_argument("--repair-calls", type=_nonnegative_int)
|
|
155
|
+
evaluate.add_argument("--token-budget", type=_positive_int)
|
|
156
|
+
evaluate.add_argument("--cost-budget", type=_positive_float)
|
|
157
|
+
evaluate.add_argument("--fail-fast", action="store_true")
|
|
158
|
+
evaluate.add_argument("--runtime", choices=("auto", "subprocess", "docker"), default="auto")
|
|
159
|
+
evaluate.add_argument("--runtime-image")
|
|
160
|
+
evaluate.add_argument("--runtime-network", choices=("public", "none"))
|
|
161
|
+
evaluate.add_argument("--runtime-cpu", type=_positive_float)
|
|
162
|
+
evaluate.add_argument("--runtime-memory-mb", type=_positive_int)
|
|
163
|
+
evaluate.add_argument("--runtime-pids", type=_positive_int)
|
|
164
|
+
evaluate.add_argument(
|
|
165
|
+
"--runtime-pull", choices=("missing", "always", "never"), default="missing"
|
|
166
|
+
)
|
|
167
|
+
evaluate.add_argument("--json", action="store_true", help="print the result as JSON")
|
|
168
|
+
evaluate.add_argument(
|
|
169
|
+
"--effort",
|
|
170
|
+
choices=[str(item) for item in ReasoningEffort],
|
|
171
|
+
default=None,
|
|
172
|
+
help="program-time budget dial. Sets model/tool call, wall-time, and token "
|
|
173
|
+
"allowances together. Any budget you pass explicitly is preserved. Effort "
|
|
174
|
+
"permits more reasoning; it does not promise a better score.",
|
|
175
|
+
)
|
|
176
|
+
evaluate.set_defaults(handler=_run_eval)
|
|
177
|
+
|
|
178
|
+
login = commands.add_parser(
|
|
179
|
+
"login",
|
|
180
|
+
help="sign in to your CoreThink account through the browser",
|
|
181
|
+
description="Authorize this CLI installation against your CoreThink account. "
|
|
182
|
+
"With no options it opens the browser you already sign in with; the CLI "
|
|
183
|
+
"never sees your Google credential. The options below configure endpoints "
|
|
184
|
+
"and a model provider's key instead, which is a separate thing and stays "
|
|
185
|
+
"separate.",
|
|
186
|
+
)
|
|
187
|
+
login.add_argument(
|
|
188
|
+
"--provider-key",
|
|
189
|
+
action="store_true",
|
|
190
|
+
help="prompt for a MODEL PROVIDER's key (OpenRouter and the like) and store "
|
|
191
|
+
"it for direct provider evaluation. Not a CoreThink credential and never "
|
|
192
|
+
"sent to CoreThink.",
|
|
193
|
+
)
|
|
194
|
+
login.add_argument(
|
|
195
|
+
"--api-key",
|
|
196
|
+
help=argparse.SUPPRESS, # the old, ambiguous spelling of --provider-key
|
|
197
|
+
)
|
|
198
|
+
login.add_argument("--api-url", help="the CoreThink API to sign in to")
|
|
199
|
+
login.add_argument(
|
|
200
|
+
"--no-browser",
|
|
201
|
+
action="store_true",
|
|
202
|
+
help="print the URL instead of opening it, for a machine with no browser",
|
|
203
|
+
)
|
|
204
|
+
login.add_argument("--base-url", help="model endpoint, used when --base-url is not passed")
|
|
205
|
+
login.add_argument("--model", help="default model")
|
|
206
|
+
login.add_argument("--registry-url", help="hosted program registry")
|
|
207
|
+
login.add_argument("--hub-url", help="environments hub")
|
|
208
|
+
login.add_argument(
|
|
209
|
+
"--hub-token",
|
|
210
|
+
action="store_true",
|
|
211
|
+
help="securely prompt for a temporary environment access token from Workbench",
|
|
212
|
+
)
|
|
213
|
+
login.set_defaults(handler=_run_login)
|
|
214
|
+
|
|
215
|
+
logout = commands.add_parser("logout", help="remove the stored credential")
|
|
216
|
+
logout.add_argument(
|
|
217
|
+
"--all",
|
|
218
|
+
action="store_true",
|
|
219
|
+
help="remove every stored setting, not only the credential",
|
|
220
|
+
)
|
|
221
|
+
logout.set_defaults(handler=_run_logout)
|
|
222
|
+
|
|
223
|
+
config = commands.add_parser("config", help="show or change stored settings")
|
|
224
|
+
config_commands = config.add_subparsers(dest="config_command", required=True)
|
|
225
|
+
config_list = config_commands.add_parser("list", help="every setting and where it came from")
|
|
226
|
+
config_list.add_argument("--json", action="store_true")
|
|
227
|
+
config_list.set_defaults(handler=_run_config_list)
|
|
228
|
+
config_get = config_commands.add_parser("get", help="one setting's value in force")
|
|
229
|
+
config_get.add_argument("key")
|
|
230
|
+
config_get.set_defaults(handler=_run_config_get)
|
|
231
|
+
config_set = config_commands.add_parser("set", help="store one setting")
|
|
232
|
+
config_set.add_argument("key")
|
|
233
|
+
config_set.add_argument("value")
|
|
234
|
+
config_set.set_defaults(handler=_run_config_set)
|
|
235
|
+
|
|
236
|
+
models = commands.add_parser("models", help="the endpoints this CLI can reach")
|
|
237
|
+
models_commands = models.add_subparsers(dest="models_command", required=True)
|
|
238
|
+
models_list = models_commands.add_parser("list", help="named endpoints and their readiness")
|
|
239
|
+
models_list.add_argument("--json", action="store_true")
|
|
240
|
+
models_list.set_defaults(handler=_run_models_list)
|
|
241
|
+
|
|
242
|
+
program = commands.add_parser("program", help="create portable reasoning programs")
|
|
243
|
+
program_commands = program.add_subparsers(dest="program_command", required=True)
|
|
244
|
+
initialize = program_commands.add_parser("init", help="create an editable program.json")
|
|
245
|
+
initialize.add_argument("path", help="target JSON file or directory")
|
|
246
|
+
initialize.add_argument("--program-id")
|
|
247
|
+
initialize.add_argument("--program-version", default=DEFAULT_PROGRAM_VERSION)
|
|
248
|
+
initialize.set_defaults(handler=_run_program_init)
|
|
249
|
+
|
|
250
|
+
from corethink import account
|
|
251
|
+
|
|
252
|
+
account.register(commands)
|
|
253
|
+
|
|
254
|
+
experiment_cli.register(commands, program_commands)
|
|
255
|
+
jobs_cli.register(commands)
|
|
256
|
+
from corethink import rl_cli
|
|
257
|
+
|
|
258
|
+
rl_cli.register(commands)
|
|
259
|
+
|
|
260
|
+
trace = commands.add_parser("trace", help="inspect evaluation artifacts")
|
|
261
|
+
trace_commands = trace.add_subparsers(dest="trace_command", required=True)
|
|
262
|
+
show = trace_commands.add_parser("show", help="show one completed run")
|
|
263
|
+
show.add_argument("run_id")
|
|
264
|
+
show.add_argument("--out", default=".corethink", help="artifact root directory")
|
|
265
|
+
show.add_argument("--json", action="store_true", help="include all events as JSON")
|
|
266
|
+
show.set_defaults(handler=_show_trace)
|
|
267
|
+
|
|
268
|
+
export = trace_commands.add_parser("export", help="export a redacted audit bundle for one run")
|
|
269
|
+
export.add_argument("run_id")
|
|
270
|
+
export.add_argument("--out", default=".", help="artifact root containing runs/")
|
|
271
|
+
export.add_argument("--to", default=None, help="write the bundle to this path")
|
|
272
|
+
export.add_argument("--request-id", default=None, help="production request this run answered")
|
|
273
|
+
export.add_argument("--received-at", default=None, help="when that request arrived")
|
|
274
|
+
export.add_argument("--subject-reference", default=None, help="opaque customer reference")
|
|
275
|
+
export.add_argument("--channel", default=None, help="how the request arrived")
|
|
276
|
+
export.add_argument(
|
|
277
|
+
"--all-events",
|
|
278
|
+
action="store_true",
|
|
279
|
+
help="include operational events outside the audit set",
|
|
280
|
+
)
|
|
281
|
+
export.add_argument("--json", action="store_true")
|
|
282
|
+
export.set_defaults(handler=_export_audit)
|
|
283
|
+
return parser
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
287
|
+
parser = build_parser()
|
|
288
|
+
args = parser.parse_args(argv)
|
|
289
|
+
try:
|
|
290
|
+
return int(args.handler(args))
|
|
291
|
+
except (
|
|
292
|
+
ImportError,
|
|
293
|
+
OSError,
|
|
294
|
+
ValueError,
|
|
295
|
+
AccountError,
|
|
296
|
+
ConfigError,
|
|
297
|
+
ExperimentError,
|
|
298
|
+
JobsError,
|
|
299
|
+
) as error:
|
|
300
|
+
print(f"corethink: configuration error: {error}", file=sys.stderr)
|
|
301
|
+
return 2
|
|
302
|
+
except Exception as error:
|
|
303
|
+
print(f"corethink: evaluation infrastructure failed: {error}", file=sys.stderr)
|
|
304
|
+
return 1
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _live_model_client(args: argparse.Namespace) -> OpenAICompatibleModelClient:
|
|
308
|
+
"""The `--adapter live` transport, authenticated only when a key is configured.
|
|
309
|
+
|
|
310
|
+
The key comes from an environment variable, never an argument, so it never
|
|
311
|
+
lands in a shell history or process listing. Absent means unauthenticated —
|
|
312
|
+
correct for the public CoreThink Reasoner, and the reason this must never
|
|
313
|
+
default to requiring a key: that would break the one endpoint that needs none.
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
# The named environment variable first, because a caller who passed
|
|
317
|
+
# --model-api-key-env is naming where the key lives for this run. Then the
|
|
318
|
+
# stored credential, which is what `corethink login` exists to provide.
|
|
319
|
+
api_key = os.environ.get(args.model_api_key_env, "").strip() or (_provider_key() or "")
|
|
320
|
+
http_client = httpx.Client(headers={"Authorization": f"Bearer {api_key}"}) if api_key else None
|
|
321
|
+
return OpenAICompatibleModelClient(
|
|
322
|
+
_resolved(args, "base_url", DEFAULT_BASE_URL),
|
|
323
|
+
client=http_client,
|
|
324
|
+
capture_logprobs=getattr(args, "capture_logprobs", False),
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _provider_key() -> str | None:
|
|
329
|
+
"""The model provider's key, with a broken config treated as unset.
|
|
330
|
+
|
|
331
|
+
Never the CoreThink account credential: that lives in its own file, has its
|
|
332
|
+
own prefix, and is refused by `corethink.account` if it ever turns up here.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
from corethink.config import ConfigError, provider_key
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
return provider_key()
|
|
339
|
+
except ConfigError:
|
|
340
|
+
return None
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _stored(key: str) -> str | None:
|
|
344
|
+
"""One stored setting, with a missing or unreadable config treated as unset.
|
|
345
|
+
|
|
346
|
+
A broken config file must not stop an evaluation that was fully specified on
|
|
347
|
+
the command line; it is a convenience layer, and a convenience that can
|
|
348
|
+
prevent work is not one.
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
from corethink.config import ConfigError, resolve
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
return resolve(key)
|
|
355
|
+
except ConfigError:
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _resolved(args: argparse.Namespace, key: str, fallback: str | None = None) -> str | None:
|
|
360
|
+
"""Flag, then environment variable, then stored config, then the built-in."""
|
|
361
|
+
|
|
362
|
+
return getattr(args, key, None) or _stored(key) or fallback
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _sampling_config(args: argparse.Namespace) -> dict:
|
|
366
|
+
"""Sampling parameters, as part of the program's identity.
|
|
367
|
+
|
|
368
|
+
Only what was asked for is included, so an unset option leaves both the request
|
|
369
|
+
payload and the program hash exactly as earlier clients produced them.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
config: dict = {"temperature": getattr(args, "temperature", 0) or 0}
|
|
373
|
+
top_p = getattr(args, "top_p", None)
|
|
374
|
+
if top_p is not None:
|
|
375
|
+
config["top_p"] = top_p
|
|
376
|
+
seed = getattr(args, "seed", None)
|
|
377
|
+
if seed is not None:
|
|
378
|
+
# A seed makes stochastic sampling reproducible, which sounds strictly good
|
|
379
|
+
# and is not: k samples of one prompt under one seed are k identical
|
|
380
|
+
# answers. Useful for reproducing a single rollout, useless for building a
|
|
381
|
+
# group, so it is opt-in and never defaulted.
|
|
382
|
+
config["seed"] = seed
|
|
383
|
+
return config
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _run_eval(args: argparse.Namespace) -> int:
|
|
387
|
+
if args.program and any(
|
|
388
|
+
value is not None
|
|
389
|
+
for value in (args.program_id, args.program_version, args.system_instructions)
|
|
390
|
+
):
|
|
391
|
+
raise ValueError(
|
|
392
|
+
"--program cannot be combined with --program-id, --program-version, "
|
|
393
|
+
"or --system-instructions"
|
|
394
|
+
)
|
|
395
|
+
_apply_agent_preset(args)
|
|
396
|
+
group_size = getattr(args, "group", 1) or 1
|
|
397
|
+
if group_size > 1:
|
|
398
|
+
# The *effective* temperature, not the flag. `--program` supplies its own
|
|
399
|
+
# config, and a program pinned at temperature 0 made `--temperature 1.0`
|
|
400
|
+
# a no-op: the guard passed on the flag, every sample of a prompt was
|
|
401
|
+
# identical, and 70 tasks came back perfectly flat — which reads as a
|
|
402
|
+
# taskset with no headroom rather than as sampling that never varied.
|
|
403
|
+
temperature = _effective_temperature(args)
|
|
404
|
+
if temperature <= 0:
|
|
405
|
+
raise ValueError(
|
|
406
|
+
"--group requires --temperature above 0. At temperature 0 every "
|
|
407
|
+
"sample of a prompt is the same completion, so a group of K is K "
|
|
408
|
+
"copies of one answer: identical rewards, zero advantage, nothing "
|
|
409
|
+
"to learn from. Refused here rather than after K model calls have "
|
|
410
|
+
"been paid for."
|
|
411
|
+
)
|
|
412
|
+
if getattr(args, "seed", None) is not None:
|
|
413
|
+
raise ValueError(
|
|
414
|
+
"--group cannot be combined with --seed. One seed makes every "
|
|
415
|
+
"sample of a prompt identical, which is exactly what a group must "
|
|
416
|
+
"not be. Seed a single rollout to reproduce it; leave it unset to "
|
|
417
|
+
"collect a group."
|
|
418
|
+
)
|
|
419
|
+
loaded = load_taskset(args.taskset)
|
|
420
|
+
taskset = loaded.taskset
|
|
421
|
+
if args.limit is not None:
|
|
422
|
+
taskset = replace(taskset, tasks=taskset.tasks[: args.limit])
|
|
423
|
+
if not taskset.tasks:
|
|
424
|
+
raise ValueError("--limit selected no tasks")
|
|
425
|
+
|
|
426
|
+
if args.adapter == "fake":
|
|
427
|
+
if loaded.fake_client_factory is None:
|
|
428
|
+
raise ValueError(
|
|
429
|
+
f"{loaded.source} does not expose build_fake_client() required by --adapter fake"
|
|
430
|
+
)
|
|
431
|
+
client = loaded.fake_client_factory()
|
|
432
|
+
provider = "scripted"
|
|
433
|
+
model = args.model or "corethink/fake-tool-router"
|
|
434
|
+
else:
|
|
435
|
+
client = _live_model_client(args)
|
|
436
|
+
provider = "openai-compatible"
|
|
437
|
+
model = _resolved(args, "model", DEFAULT_MODEL)
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
if args.program:
|
|
441
|
+
loaded_program = load_program_definition(args.program)
|
|
442
|
+
program = loaded_program.definition.bind(provider=provider, model=model)
|
|
443
|
+
program_source = ProgramSource.manifest(loaded_program.definition.content_hash)
|
|
444
|
+
program = _with_sampling_override(program, args)
|
|
445
|
+
else:
|
|
446
|
+
program = ProgramSpec(
|
|
447
|
+
program_id=args.program_id or DEFAULT_PROGRAM_ID,
|
|
448
|
+
version=args.program_version or DEFAULT_PROGRAM_VERSION,
|
|
449
|
+
provider=provider,
|
|
450
|
+
model=model,
|
|
451
|
+
system_instructions=args.system_instructions or DEFAULT_SYSTEM_INSTRUCTIONS,
|
|
452
|
+
# Temperature belongs in config, unlike logprob capture: it changes
|
|
453
|
+
# what the program *does*, so two temperatures are two programs and
|
|
454
|
+
# the hash should say so. The default stays 0, because every
|
|
455
|
+
# evaluation to date was deterministic and silently making them
|
|
456
|
+
# stochastic would make historical scores irreproducible.
|
|
457
|
+
#
|
|
458
|
+
# It has to be reachable, though. At temperature 0 every sample of
|
|
459
|
+
# a prompt is identical, so a GRPO group of k is k copies of one
|
|
460
|
+
# answer: flat, zero advantage, no gradient at all. Sampling k
|
|
461
|
+
# *different* completions is the entire premise of a group-relative
|
|
462
|
+
# baseline, and until now there was no way to ask for it.
|
|
463
|
+
config=_sampling_config(args),
|
|
464
|
+
)
|
|
465
|
+
program_source = ProgramSource.inline()
|
|
466
|
+
config = _run_config(args)
|
|
467
|
+
# Each attempt is a full run with its own artifacts, rather than K
|
|
468
|
+
# results inside one. The corpus builder already merges run directories
|
|
469
|
+
# and groups by task id, so this reuses the whole path instead of
|
|
470
|
+
# inventing a second artifact shape that only grouped runs would use.
|
|
471
|
+
results = [
|
|
472
|
+
EvaluationRunner(
|
|
473
|
+
taskset,
|
|
474
|
+
program,
|
|
475
|
+
client,
|
|
476
|
+
config=config,
|
|
477
|
+
artifact_root=Path(args.out),
|
|
478
|
+
program_source=program_source,
|
|
479
|
+
runtime_selection=_runtime_selection(args),
|
|
480
|
+
).evaluate()
|
|
481
|
+
for _ in range(group_size)
|
|
482
|
+
]
|
|
483
|
+
result = results[-1]
|
|
484
|
+
finally:
|
|
485
|
+
close = getattr(client, "close", None)
|
|
486
|
+
if callable(close):
|
|
487
|
+
close()
|
|
488
|
+
|
|
489
|
+
if args.json:
|
|
490
|
+
payload = (
|
|
491
|
+
result.to_dict() if group_size == 1 else {"group": [item.to_dict() for item in results]}
|
|
492
|
+
)
|
|
493
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
494
|
+
elif group_size > 1:
|
|
495
|
+
_print_group(results)
|
|
496
|
+
else:
|
|
497
|
+
passed = sum(item.status is TaskStatus.PASSED for item in result.task_results)
|
|
498
|
+
aggregate = "null" if result.primary_score is None else f"{result.primary_score:.3f}"
|
|
499
|
+
print(
|
|
500
|
+
f"run {result.run_id}: score={aggregate} "
|
|
501
|
+
f"passed={passed}/{len(result.task_results)} "
|
|
502
|
+
f"scored={result.scored_task_count} errors={result.errored_task_count}"
|
|
503
|
+
)
|
|
504
|
+
for item in result.task_results:
|
|
505
|
+
score = "null" if item.score is None else f"{item.score.primary:.3f}"
|
|
506
|
+
print(
|
|
507
|
+
f" {item.task_id}: {item.status.value} "
|
|
508
|
+
f"score={score} stop={item.stop_reason.value} "
|
|
509
|
+
f"runtime={item.runtime_spec.provider if item.runtime_spec else 'unknown'}"
|
|
510
|
+
)
|
|
511
|
+
print(f"artifacts: {result.artifact_dir}")
|
|
512
|
+
return (
|
|
513
|
+
1 if any(item.errored_task_count or item.infrastructure_warnings for item in results) else 0
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _apply_agent_preset(args: argparse.Namespace) -> None:
|
|
518
|
+
"""Expand a named agent into the flags it stands for.
|
|
519
|
+
|
|
520
|
+
Applied before anything reads them, and only where the caller left a value
|
|
521
|
+
unset — naming a preset must never override something typed explicitly, or
|
|
522
|
+
`corethink eval tasks.py corethink --model other` would quietly run the
|
|
523
|
+
wrong model.
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
name = getattr(args, "agent", None)
|
|
527
|
+
if not name:
|
|
528
|
+
return
|
|
529
|
+
preset = AGENT_PRESETS.get(name)
|
|
530
|
+
if preset is None:
|
|
531
|
+
raise ValueError(
|
|
532
|
+
f"unknown agent {name!r}; known: {', '.join(sorted(AGENT_PRESETS))}. "
|
|
533
|
+
f"Pass --base-url and --model to reach anything else."
|
|
534
|
+
)
|
|
535
|
+
if preset.get("adapter") and args.adapter == "live":
|
|
536
|
+
args.adapter = preset["adapter"]
|
|
537
|
+
for key in ("base_url", "model"):
|
|
538
|
+
if preset.get(key) and not getattr(args, key, None):
|
|
539
|
+
setattr(args, key, preset[key])
|
|
540
|
+
# The key's *location* comes from the preset, never the key itself.
|
|
541
|
+
if preset.get("api_key_env") and args.model_api_key_env == DEFAULT_MODEL_API_KEY_ENV:
|
|
542
|
+
args.model_api_key_env = preset["api_key_env"]
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _effective_temperature(args: argparse.Namespace) -> float:
|
|
546
|
+
"""The temperature the request will actually carry.
|
|
547
|
+
|
|
548
|
+
A flag on the command line, or — when a program file is named and the flag
|
|
549
|
+
is not — whatever that program pins. Read here rather than at the call site
|
|
550
|
+
so the group guard and the request cannot disagree about it.
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
flag = getattr(args, "temperature", None)
|
|
554
|
+
if flag is not None:
|
|
555
|
+
return float(flag)
|
|
556
|
+
if getattr(args, "program", None):
|
|
557
|
+
try:
|
|
558
|
+
definition = load_program_definition(args.program).definition
|
|
559
|
+
except (OSError, ValueError):
|
|
560
|
+
return 0.0
|
|
561
|
+
return float(definition.config.get("temperature", 0) or 0)
|
|
562
|
+
return 0.0
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def _with_sampling_override(program, args: argparse.Namespace):
|
|
566
|
+
"""Apply `--temperature`/`--top-p` over a program file's own config.
|
|
567
|
+
|
|
568
|
+
Sampling is part of a program's identity by design — two temperatures are
|
|
569
|
+
two programs and the hash says so — so this produces a *different* program
|
|
570
|
+
rather than the named one with a changed knob, and says so on stderr. The
|
|
571
|
+
alternative was silence, which is what let a group be collected at
|
|
572
|
+
temperature 0 while the caller had asked for 1.0.
|
|
573
|
+
"""
|
|
574
|
+
|
|
575
|
+
overrides = {
|
|
576
|
+
key: getattr(args, key)
|
|
577
|
+
for key in ("temperature", "top_p")
|
|
578
|
+
if getattr(args, key, None) is not None
|
|
579
|
+
}
|
|
580
|
+
if not overrides:
|
|
581
|
+
return program
|
|
582
|
+
changed = {key: value for key, value in overrides.items() if program.config.get(key) != value}
|
|
583
|
+
if not changed:
|
|
584
|
+
return program
|
|
585
|
+
updated = replace(program, config={**program.config, **overrides})
|
|
586
|
+
print(
|
|
587
|
+
f"corethink: {args.program} pins "
|
|
588
|
+
+ ", ".join(f"{key}={program.config.get(key)!r}" for key in changed)
|
|
589
|
+
+ "; overridden to "
|
|
590
|
+
+ ", ".join(f"{key}={value!r}" for key, value in changed.items())
|
|
591
|
+
+ f". Sampling is part of a program's identity, so this is a different "
|
|
592
|
+
f"program: {updated.content_hash[:12]} rather than "
|
|
593
|
+
f"{program.content_hash[:12]}.",
|
|
594
|
+
file=sys.stderr,
|
|
595
|
+
)
|
|
596
|
+
return updated
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def _print_group(results: list) -> None:
|
|
600
|
+
"""What a group is collected for: whether any prompt's attempts disagree.
|
|
601
|
+
|
|
602
|
+
A task that *errored* is not a task that scored zero, and the two are easy
|
|
603
|
+
to confuse because the scorer still returns a `Score` for an errored task —
|
|
604
|
+
an empty answer matches nothing, so it reads 0.000. Counting those as wrong
|
|
605
|
+
answers invents disagreement out of a rate limit: eight correct completions
|
|
606
|
+
and four HTTP 429s read as a spread worth training on, which is exactly the
|
|
607
|
+
shape of failure this platform exists to refuse.
|
|
608
|
+
|
|
609
|
+
Errored attempts are therefore excluded from the numbers and reported on
|
|
610
|
+
their own, and a group missing attempts is called incomplete rather than
|
|
611
|
+
usable.
|
|
612
|
+
"""
|
|
613
|
+
|
|
614
|
+
from statistics import fmean, pstdev
|
|
615
|
+
|
|
616
|
+
per_task: dict[str, list[float]] = {}
|
|
617
|
+
errored: dict[str, int] = {}
|
|
618
|
+
for result in results:
|
|
619
|
+
for item in result.task_results:
|
|
620
|
+
if getattr(item.status, "value", str(item.status)) == "error":
|
|
621
|
+
errored[item.task_id] = errored.get(item.task_id, 0) + 1
|
|
622
|
+
continue
|
|
623
|
+
if item.score is not None:
|
|
624
|
+
per_task.setdefault(item.task_id, []).append(item.score.primary)
|
|
625
|
+
|
|
626
|
+
attempts = len(results)
|
|
627
|
+
task_ids = sorted(set(per_task) | set(errored))
|
|
628
|
+
spread = [task for task, scores in per_task.items() if len(set(scores)) > 1]
|
|
629
|
+
print(f"group of {attempts} over {len(task_ids)} task(s)")
|
|
630
|
+
for task in task_ids:
|
|
631
|
+
scores = per_task.get(task, [])
|
|
632
|
+
failures = errored.get(task, 0)
|
|
633
|
+
if not scores:
|
|
634
|
+
print(f" {task}: no scored attempt — {failures}/{attempts} errored")
|
|
635
|
+
continue
|
|
636
|
+
mean = fmean(scores)
|
|
637
|
+
deviation = pstdev(scores) if len(scores) > 1 else 0.0
|
|
638
|
+
marker = "spread" if task in spread else "flat"
|
|
639
|
+
rendered = " ".join(f"{score:.3f}" for score in scores)
|
|
640
|
+
suffix = f" ({failures} errored, excluded)" if failures else ""
|
|
641
|
+
print(f" {task}: mean={mean:.3f} sd={deviation:.3f} {marker:<6} [{rendered}]{suffix}")
|
|
642
|
+
|
|
643
|
+
total_errors = sum(errored.values())
|
|
644
|
+
if total_errors:
|
|
645
|
+
# Before the verdict, because an incomplete group is the thing most
|
|
646
|
+
# likely to be mistaken for a result.
|
|
647
|
+
print(
|
|
648
|
+
f"{total_errors} attempt(s) errored and were excluded — "
|
|
649
|
+
f"`corethink trace show <run-id>` names the cause"
|
|
650
|
+
)
|
|
651
|
+
print(
|
|
652
|
+
"incomplete: some prompts have fewer attempts than others, so the "
|
|
653
|
+
"baseline they would be compared against is not the one you asked for"
|
|
654
|
+
)
|
|
655
|
+
return
|
|
656
|
+
|
|
657
|
+
verdict = (
|
|
658
|
+
"a group-relative baseline has something to work with"
|
|
659
|
+
if spread
|
|
660
|
+
else "nothing here can produce an advantage"
|
|
661
|
+
)
|
|
662
|
+
print(f"{len(spread)}/{len(task_ids)} task(s) produced a spread — {verdict}")
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _run_models_list(args: argparse.Namespace) -> int:
|
|
666
|
+
"""Which endpoints are reachable, and which are missing a credential.
|
|
667
|
+
|
|
668
|
+
Deliberately not a catalogue of models a hosted service offers, because
|
|
669
|
+
there is no such service: these are the endpoints this CLI knows how to
|
|
670
|
+
address. Naming them here is the difference between "unknown agent" and
|
|
671
|
+
knowing what to type.
|
|
672
|
+
|
|
673
|
+
There is no `fork` to match HUD's trainable models. Creating one would be a
|
|
674
|
+
new word for machinery that already exists under a truer name, so the row
|
|
675
|
+
below points at it instead.
|
|
676
|
+
"""
|
|
677
|
+
|
|
678
|
+
rows = []
|
|
679
|
+
for name in sorted(AGENT_PRESETS):
|
|
680
|
+
preset = AGENT_PRESETS[name]
|
|
681
|
+
key_env = preset.get("api_key_env")
|
|
682
|
+
if preset.get("adapter") == "fake":
|
|
683
|
+
readiness = "ready (offline)"
|
|
684
|
+
elif key_env is None:
|
|
685
|
+
readiness = "ready (no key needed)"
|
|
686
|
+
elif os.environ.get(key_env, "").strip():
|
|
687
|
+
readiness = f"ready (${key_env})"
|
|
688
|
+
elif _provider_key():
|
|
689
|
+
readiness = "ready (stored credential)"
|
|
690
|
+
else:
|
|
691
|
+
readiness = f"needs ${key_env} or `corethink login`"
|
|
692
|
+
rows.append(
|
|
693
|
+
{
|
|
694
|
+
"agent": name,
|
|
695
|
+
"model": preset.get("model", "—"),
|
|
696
|
+
"base_url": preset.get("base_url", "—"),
|
|
697
|
+
"readiness": readiness,
|
|
698
|
+
"description": preset.get("description", ""),
|
|
699
|
+
}
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
if args.json:
|
|
703
|
+
print(json.dumps(rows, indent=2, sort_keys=True))
|
|
704
|
+
return 0
|
|
705
|
+
|
|
706
|
+
print("named endpoints — use as `corethink eval <taskset> <agent>`")
|
|
707
|
+
for row in rows:
|
|
708
|
+
print(f" {row['agent']:<12} {row['model']:<34} {row['readiness']}")
|
|
709
|
+
print(f" {'':<12} {row['description']}")
|
|
710
|
+
stored_model = _stored("model")
|
|
711
|
+
if stored_model:
|
|
712
|
+
print(f" (config: model={stored_model}, base_url={_stored('base_url') or '—'})")
|
|
713
|
+
print(
|
|
714
|
+
" a trained adapter is not created here: `corethink jobs submit --train-on "
|
|
715
|
+
"corpus.jsonl` produces one, and `jobs adopt` makes it a governed candidate"
|
|
716
|
+
)
|
|
717
|
+
print(
|
|
718
|
+
" a free-tier model can be withdrawn without notice; if every task scores "
|
|
719
|
+
"0.000 flat, read the trace — a 404 is recorded as model_attempt_error"
|
|
720
|
+
)
|
|
721
|
+
return 0
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def _run_login(args: argparse.Namespace) -> int:
|
|
725
|
+
"""Sign in to a CoreThink account, or configure the things that are not one.
|
|
726
|
+
|
|
727
|
+
One command with two jobs, because `login` is the word people reach for and
|
|
728
|
+
splitting it would leave the old spelling doing nothing. The rule is stated
|
|
729
|
+
rather than guessed at: options mean "store this setting", no options means
|
|
730
|
+
"sign me in". What is gone is the old default — a bare `login` that prompted
|
|
731
|
+
for a credential without ever saying which service it belonged to.
|
|
732
|
+
"""
|
|
733
|
+
|
|
734
|
+
from getpass import getpass
|
|
735
|
+
|
|
736
|
+
from corethink.account import AccountError, run_login
|
|
737
|
+
from corethink.config import CONFIG_KEYS, load_config, masked, save_config
|
|
738
|
+
|
|
739
|
+
values = load_config()
|
|
740
|
+
stored_any = False
|
|
741
|
+
provider_key = None
|
|
742
|
+
if args.api_key:
|
|
743
|
+
# Accepted, never silently: it used to mean the provider key, and a
|
|
744
|
+
# credential quietly filed under a new meaning is the failure this whole
|
|
745
|
+
# change exists to prevent.
|
|
746
|
+
print(
|
|
747
|
+
"note: --api-key means a model provider's key and is now --provider-key. "
|
|
748
|
+
"Storing it as provider_api_key; it is never sent to CoreThink.",
|
|
749
|
+
file=sys.stderr,
|
|
750
|
+
)
|
|
751
|
+
provider_key = args.api_key
|
|
752
|
+
elif args.provider_key:
|
|
753
|
+
provider_key = getpass(
|
|
754
|
+
"Model provider API key (input hidden, blank to keep the stored one): "
|
|
755
|
+
).strip()
|
|
756
|
+
if provider_key:
|
|
757
|
+
values["provider_api_key"] = provider_key
|
|
758
|
+
stored_any = True
|
|
759
|
+
if args.hub_token:
|
|
760
|
+
hub_token = getpass("Temporary environment token (input hidden): ").strip()
|
|
761
|
+
if not hub_token:
|
|
762
|
+
raise ValueError("No environment token provided")
|
|
763
|
+
values["hub_token"] = hub_token
|
|
764
|
+
stored_any = True
|
|
765
|
+
|
|
766
|
+
for key in ("base_url", "model", "registry_url", "hub_url", "api_url"):
|
|
767
|
+
supplied = getattr(args, key, None)
|
|
768
|
+
if supplied:
|
|
769
|
+
values[key] = supplied
|
|
770
|
+
stored_any = True
|
|
771
|
+
|
|
772
|
+
if not stored_any:
|
|
773
|
+
try:
|
|
774
|
+
return run_login(args)
|
|
775
|
+
except AccountError as error:
|
|
776
|
+
raise ValueError(str(error)) from None
|
|
777
|
+
|
|
778
|
+
written = save_config(values)
|
|
779
|
+
print(f"stored: {written}")
|
|
780
|
+
for key in sorted(values):
|
|
781
|
+
if key in CONFIG_KEYS:
|
|
782
|
+
print(f" {key:<17} {masked(key, str(values[key]))}")
|
|
783
|
+
print(" (file mode 0600; no command prints a credential back in full)")
|
|
784
|
+
if provider_key:
|
|
785
|
+
print(" provider keys are used for direct provider evaluation only.")
|
|
786
|
+
print(" Run `corethink login` with no options to sign in to CoreThink.")
|
|
787
|
+
return 0
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _run_logout(args: argparse.Namespace) -> int:
|
|
791
|
+
"""Sign out of CoreThink and remove the credentials stored on this machine.
|
|
792
|
+
|
|
793
|
+
Both halves, every time. Somebody typing `logout` wants this laptop to stop
|
|
794
|
+
being able to act as them, and leaving either the CoreThink session or the
|
|
795
|
+
provider key behind would not be that.
|
|
796
|
+
"""
|
|
797
|
+
|
|
798
|
+
from corethink.account import AccountError, load_credential
|
|
799
|
+
from corethink.account import run_logout as account_logout
|
|
800
|
+
from corethink.config import config_path, load_config, save_config
|
|
801
|
+
|
|
802
|
+
if load_credential():
|
|
803
|
+
try:
|
|
804
|
+
account_logout(args)
|
|
805
|
+
except AccountError as error:
|
|
806
|
+
# The local credential still goes. A CLI that kept a usable token
|
|
807
|
+
# because it could not reach the network has not logged anybody out.
|
|
808
|
+
print(f"note: {error}", file=sys.stderr)
|
|
809
|
+
|
|
810
|
+
values = load_config()
|
|
811
|
+
if not values:
|
|
812
|
+
print(f"nothing stored at {config_path()}")
|
|
813
|
+
return 0
|
|
814
|
+
if args.all:
|
|
815
|
+
remaining: dict = {}
|
|
816
|
+
else:
|
|
817
|
+
remaining = {
|
|
818
|
+
key: value
|
|
819
|
+
for key, value in values.items()
|
|
820
|
+
if key not in {"api_key", "provider_api_key", "hub_token"}
|
|
821
|
+
}
|
|
822
|
+
save_config(remaining)
|
|
823
|
+
print(f"removed {'every setting' if args.all else 'stored credentials'} from {config_path()}")
|
|
824
|
+
return 0
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def _run_config_list(args: argparse.Namespace) -> int:
|
|
828
|
+
from corethink.config import CONFIG_KEYS, config_path, describe, masked
|
|
829
|
+
|
|
830
|
+
settings = describe()
|
|
831
|
+
if args.json:
|
|
832
|
+
print(
|
|
833
|
+
json.dumps(
|
|
834
|
+
{
|
|
835
|
+
setting.key: {
|
|
836
|
+
"value": None
|
|
837
|
+
if setting.value is None
|
|
838
|
+
else masked(setting.key, setting.value),
|
|
839
|
+
"source": setting.source,
|
|
840
|
+
}
|
|
841
|
+
for setting in settings
|
|
842
|
+
},
|
|
843
|
+
indent=2,
|
|
844
|
+
sort_keys=True,
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
return 0
|
|
848
|
+
print(f"config: {config_path()}")
|
|
849
|
+
for setting in settings:
|
|
850
|
+
shown = "—" if setting.value is None else masked(setting.key, setting.value)
|
|
851
|
+
# The source is the useful column: a stale environment variable beating
|
|
852
|
+
# the file someone just edited is the confusion this exists to end.
|
|
853
|
+
print(f" {setting.key:<13} {shown:<28} {setting.source}")
|
|
854
|
+
print(f" ({len(CONFIG_KEYS)} settings; an environment variable always wins over the file)")
|
|
855
|
+
return 0
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
def _run_config_get(args: argparse.Namespace) -> int:
|
|
859
|
+
from corethink.config import resolve
|
|
860
|
+
|
|
861
|
+
value = resolve(args.key)
|
|
862
|
+
if value is None:
|
|
863
|
+
print(f"{args.key} is unset", file=sys.stderr)
|
|
864
|
+
return 1
|
|
865
|
+
# Printed in full, unmasked, because this is the command whose entire job is
|
|
866
|
+
# to hand the value to something else.
|
|
867
|
+
print(value)
|
|
868
|
+
return 0
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def _run_config_set(args: argparse.Namespace) -> int:
|
|
872
|
+
from corethink.config import CONFIG_KEYS, load_config, masked, save_config
|
|
873
|
+
|
|
874
|
+
if args.key not in CONFIG_KEYS:
|
|
875
|
+
raise ValueError(f"unknown setting {args.key!r}; known: {', '.join(sorted(CONFIG_KEYS))}")
|
|
876
|
+
values = load_config()
|
|
877
|
+
values[args.key] = args.value
|
|
878
|
+
written = save_config(values)
|
|
879
|
+
print(f"{args.key} = {masked(args.key, args.value)} ({written})")
|
|
880
|
+
return 0
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _run_program_init(args: argparse.Namespace) -> int:
|
|
884
|
+
loaded = initialize_program_definition(
|
|
885
|
+
args.path,
|
|
886
|
+
program_id=args.program_id,
|
|
887
|
+
version=args.program_version,
|
|
888
|
+
)
|
|
889
|
+
print(f"created program: {loaded.source}")
|
|
890
|
+
print(f"next: corethink eval <taskset> --program {loaded.source}")
|
|
891
|
+
return 0
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def _show_trace(args: argparse.Namespace) -> int:
|
|
895
|
+
reader = ArtifactReader(args.out, args.run_id)
|
|
896
|
+
manifest = reader.manifest()
|
|
897
|
+
result = reader.result()
|
|
898
|
+
events = reader.events()
|
|
899
|
+
if args.json:
|
|
900
|
+
print(
|
|
901
|
+
json.dumps(
|
|
902
|
+
{
|
|
903
|
+
"manifest": manifest,
|
|
904
|
+
"result": result.to_dict(),
|
|
905
|
+
"events": [event.to_dict() for event in events],
|
|
906
|
+
},
|
|
907
|
+
indent=2,
|
|
908
|
+
sort_keys=True,
|
|
909
|
+
)
|
|
910
|
+
)
|
|
911
|
+
return 0
|
|
912
|
+
|
|
913
|
+
requested = manifest.get("requested_program", {})
|
|
914
|
+
print(f"run {result.run_id}: taskset={result.taskset_id}@{result.taskset_version}")
|
|
915
|
+
print(
|
|
916
|
+
"requested program: "
|
|
917
|
+
f"{requested.get('program_id')}@{requested.get('version')} "
|
|
918
|
+
f"model={requested.get('model')} sha256={requested.get('content_hash')}"
|
|
919
|
+
)
|
|
920
|
+
print(f"events: {len(events)} usage: {json.dumps(result.usage.to_dict(), sort_keys=True)}")
|
|
921
|
+
print("timeline:")
|
|
922
|
+
for event in events:
|
|
923
|
+
duration = f" {event.duration_ms:.1f}ms" if event.duration_ms is not None else ""
|
|
924
|
+
print(f" [{event.sequence:04d}] {event.task_id} {event.kind}{duration}")
|
|
925
|
+
for item in result.task_results:
|
|
926
|
+
identities = [identity.to_dict() for identity in item.observed_identities]
|
|
927
|
+
print(f"\n{item.task_id}: {item.status.value} / {item.stop_reason.value}")
|
|
928
|
+
score = item.score.to_dict() if item.score else None
|
|
929
|
+
print(f" score: {json.dumps(score, sort_keys=True)}")
|
|
930
|
+
print(f" final state: {json.dumps(item.final_state.to_dict(), sort_keys=True)}")
|
|
931
|
+
print(f" observed engine: {json.dumps(identities, sort_keys=True)}")
|
|
932
|
+
requested_runtime = item.runtime_spec.to_dict() if item.runtime_spec else None
|
|
933
|
+
observed_runtime = item.runtime_info.to_dict() if item.runtime_info else None
|
|
934
|
+
print(
|
|
935
|
+
" runtime: "
|
|
936
|
+
f"requested={json.dumps(requested_runtime, sort_keys=True)} "
|
|
937
|
+
f"observed={json.dumps(observed_runtime, sort_keys=True)}"
|
|
938
|
+
)
|
|
939
|
+
print(f" cleanup: ok={item.cleanup_ok}")
|
|
940
|
+
return 0
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def _export_audit(args: argparse.Namespace) -> int:
|
|
944
|
+
reader = ArtifactReader(args.out, args.run_id)
|
|
945
|
+
correlation = None
|
|
946
|
+
if args.request_id:
|
|
947
|
+
correlation = RequestCorrelation(
|
|
948
|
+
request_id=args.request_id,
|
|
949
|
+
received_at=args.received_at or _iso_now(),
|
|
950
|
+
subject_reference=args.subject_reference,
|
|
951
|
+
channel=args.channel,
|
|
952
|
+
)
|
|
953
|
+
bundle = build_audit_bundle(
|
|
954
|
+
reader,
|
|
955
|
+
run_id=args.run_id,
|
|
956
|
+
correlation=correlation,
|
|
957
|
+
include_all_events=args.all_events,
|
|
958
|
+
)
|
|
959
|
+
if args.to:
|
|
960
|
+
path = write_audit_bundle(bundle, Path(args.to))
|
|
961
|
+
if args.json:
|
|
962
|
+
print(json.dumps({"path": str(path), "content_hash": bundle.content_hash}, indent=2))
|
|
963
|
+
else:
|
|
964
|
+
print(f"wrote audit bundle: {path}")
|
|
965
|
+
print(f"digest: {bundle.content_hash}")
|
|
966
|
+
for note in bundle.omissions:
|
|
967
|
+
print(f"omission: {note}")
|
|
968
|
+
return 0
|
|
969
|
+
|
|
970
|
+
if args.json:
|
|
971
|
+
print(json.dumps(bundle.to_dict(), indent=2, sort_keys=True))
|
|
972
|
+
else:
|
|
973
|
+
print(f"run: {bundle.run_id}")
|
|
974
|
+
print(f"digest: {bundle.content_hash}")
|
|
975
|
+
if bundle.correlation:
|
|
976
|
+
print(f"request: {bundle.correlation.request_id}")
|
|
977
|
+
print(f"events: {len(bundle.events)}")
|
|
978
|
+
for note in bundle.omissions:
|
|
979
|
+
print(f"omission: {note}")
|
|
980
|
+
return 0
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def _iso_now() -> str:
|
|
984
|
+
import datetime as _dt
|
|
985
|
+
|
|
986
|
+
return _dt.datetime.now(tz=_dt.UTC).isoformat().replace("+00:00", "Z")
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _run_config(args: argparse.Namespace) -> RunConfig:
|
|
990
|
+
overrides = {
|
|
991
|
+
"max_model_calls": args.max_model_calls,
|
|
992
|
+
"max_tool_calls": args.max_tool_calls,
|
|
993
|
+
"task_timeout_seconds": args.task_timeout,
|
|
994
|
+
"model_timeout_seconds": args.model_timeout,
|
|
995
|
+
"tool_timeout_seconds": args.tool_timeout,
|
|
996
|
+
"transient_retries": args.retries,
|
|
997
|
+
"malformed_repair_calls": args.repair_calls,
|
|
998
|
+
"token_budget": args.token_budget,
|
|
999
|
+
"cost_budget": args.cost_budget,
|
|
1000
|
+
"fail_fast": args.fail_fast,
|
|
1001
|
+
}
|
|
1002
|
+
explicit = {name: value for name, value in overrides.items() if value is not None}
|
|
1003
|
+
config = RunConfig(**explicit)
|
|
1004
|
+
|
|
1005
|
+
effort = getattr(args, "effort", None)
|
|
1006
|
+
if effort is None:
|
|
1007
|
+
return config
|
|
1008
|
+
|
|
1009
|
+
# Pin only the dial-controlled budgets the caller actually set. Store-true
|
|
1010
|
+
# flags like --fail-fast are always present in `explicit` and are none of the
|
|
1011
|
+
# dial's business, so they must not be offered as pinned fields.
|
|
1012
|
+
pinned = tuple(name for name in explicit if name in DIAL_FIELDS)
|
|
1013
|
+
resolution = resolve_effort(effort, base=config, pinned=pinned)
|
|
1014
|
+
if not getattr(args, "json", False):
|
|
1015
|
+
print(f"effort: {resolution.effort}")
|
|
1016
|
+
if resolution.caller_pinned:
|
|
1017
|
+
print(f" preserved: {', '.join(resolution.caller_pinned)}")
|
|
1018
|
+
print(f" {resolution.notice}")
|
|
1019
|
+
return resolution.config
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
def _runtime_selection(args: argparse.Namespace) -> RuntimeSelection:
|
|
1023
|
+
return RuntimeSelection(
|
|
1024
|
+
provider=args.runtime,
|
|
1025
|
+
image=args.runtime_image,
|
|
1026
|
+
network=NetworkMode(args.runtime_network) if args.runtime_network else None,
|
|
1027
|
+
resources=RuntimeResources(
|
|
1028
|
+
cpu=args.runtime_cpu,
|
|
1029
|
+
memory_mb=args.runtime_memory_mb,
|
|
1030
|
+
pids=args.runtime_pids,
|
|
1031
|
+
),
|
|
1032
|
+
pull_policy=PullPolicy(args.runtime_pull),
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
def _positive_int(value: str) -> int:
|
|
1037
|
+
parsed = int(value)
|
|
1038
|
+
if parsed <= 0:
|
|
1039
|
+
raise argparse.ArgumentTypeError("must be positive")
|
|
1040
|
+
return parsed
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _nonnegative_int(value: str) -> int:
|
|
1044
|
+
parsed = int(value)
|
|
1045
|
+
if parsed < 0:
|
|
1046
|
+
raise argparse.ArgumentTypeError("must be nonnegative")
|
|
1047
|
+
return parsed
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
def _positive_float(value: str) -> float:
|
|
1051
|
+
parsed = float(value)
|
|
1052
|
+
if parsed <= 0:
|
|
1053
|
+
raise argparse.ArgumentTypeError("must be positive")
|
|
1054
|
+
return parsed
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
if __name__ == "__main__":
|
|
1058
|
+
raise SystemExit(main())
|