Repository navigation
feat(harness): build an agent from a repo's subfolder - #225
khushalsonawat wants to merge 168 commits into
Conversation
753c3ce to
7b09699
Compare
| @model_validator(mode="after") | ||
| def _required_locator(self) -> "RepositorySource": | ||
| if self.subfolder and self.kind is not SourceKind.GITHUB: | ||
| raise ValueError("subfolder_requires_github_source") |
There was a problem hiding this comment.
A subfolder on a non-GitHub source crashes instead of refusing (P3).
subfolder_requires_github_source fails job parsing, so the hosted run exits 2 (crashed) with no terminal failure, and authoring main exits 1 with no failure.json. The platform sees a crash, not a clear reason.
Related dead branch: scenario_source.py echoes the folder for ARCHIVE sources too, which can never carry one.
Fix: report it as subfolder_not_found (or its own code) through the failure file and terminal event, and drop ARCHIVE from the echo branch.
There was a problem hiding this comment.
Not changed. #3661's serializer refuses this with a 400 first, so only a hand-written job reaches the parse, and a bad job file already exits as crashed by design. The ARCHIVE branch can't be dropped: it builds the whole document for uploads, not just the echo. Leaving this open for you.
| contract, subfolder=job.source.subfolder | ||
| ), | ||
| } | ||
| except Exception as exc: |
There was a problem hiding this comment.
A failed document build reaches the platform as subfolder_ignored (P3).
Any exception from build_authored_agent is only logged and registration continues with no document. On a subfolder build the platform then fails the job with subfolder_ignored, which hides the real cause (for example an AgentBuildError).
Fix: when job.source.subfolder is set, re-raise (or fail with the real code) instead of continuing without the document.
There was a problem hiding this comment.
Not changed. At #3661 225536bf0, _check_subfolder_echo returns early when no document was sent, so a failed build can't surface as subfolder_ignored. Failing a folder build whenever the document can't be built would be a new behaviour. Leaving this open for you.
| logger.error("job.json invalid: %s", exc) | ||
| return EXIT_CRASHED | ||
|
|
||
| if (unusable := folder_failure(source, job.source.subfolder)) is not None: |
There was a problem hiding this comment.
On the run path the folder failure loses retryable and details (P3).
_fail builds the terminal failure from domain, stage, code and message only, so details.subfolder (and retryable: false) never reach the platform for a run that reports through the terminal event on exit 0. It isn't retried either way, so this is about what the user and logs see.
Fix: let _fail pass retryable and details through, and send them here from unusable.
There was a problem hiding this comment.
Not changed. HarnessFailureSerializer at #3661 has only domain, stage, code and message, and only the failure-file path adds retryable and details, so as I read it these keys wouldn't reach the platform from the terminal event. It isn't retried either way. Leaving this open for you.
107ee20 to
9d3bf78
Compare
95c863c to
f9d3452
Compare
a62389f to
3e4e3ec
Compare
| folder = agent_folder(root, subfolder) | ||
| except SubfolderNotFound: | ||
| return frozenset() | ||
| python_bases = (folder, folder / "src", root, root / "src") |
There was a problem hiding this comment.
Keys read by a src/-layout uv workspace package become optional (P3).
python_bases is only the folder, folder/src, the root and root/src, so workspace members are never resolved.
Checked: root pyproject.toml with [tool.uv.workspace] members = ["agents/a", "packages/shared"], agents/a/agent.py doing from shared.store import db, and packages/shared/src/shared/store.py reading SHARED_DB_PASSWORD. A root build marks it required; agent_subfolder="agents/a" marks it optional. Known limits only mentions this for JS/TS, but it's a common Python monorepo layout.
Fix: add each workspace member's root and src/ from [tool.uv.workspace].members to python_bases, or list the limit.
There was a problem hiding this comment.
Kept as a known limit, by Khushal's call. A package that only a uv workspace (or a path alias) can resolve isn't followed, so the keys it reads stay optional, and a missing one fails at the agent's first call. It's listed in the description under Known limits, "Imports the walk can't see".
One repo can hold several agents. A job can name one with source.subfolder: the whole repo stays the build root and the folder narrows where the agent is found, run from and read from. The value comes back as extension.source.subfolder. A folder that can't be used fails with subfolder_not_found before any model call and exits 78, so the platform doesn't retry it. With no folder builds are byte-identical. TH-8804 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
In a repo with several agents, a sibling's browser tool would trigger a second look on every build of this one. A missing folder counts as nothing found, because the folder check reports it. Use the refusal exit code from agent_type_check rather than a second copy. TH-8804 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
- Check a tool in the agent's folder first, where the run imports from. - Require credentials from what ships and what the folder imports, and list a sibling agent's as optional rather than dropping them. - Refuse a malformed folder value as subfolder_not_found. - Echo the folder the bundle was built from, and send no document when it differs from the job's. - Answer when the source root path goes through a link. - Keep one definition of the folder helpers in source_folder.py. TH-8804 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
- The browser-tool check also reads what the folder imports and the files directly above it, still ignoring sibling agents. - The import walk skips links and anything but regular files, and keeps to the credential scan's size and file limits. - Follow JavaScript and TypeScript imports written as a path. - A Compose file above the folder requires values only when the runtime names it. TH-8804 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
- Write extension.framework = {name, version} on every build, plus label
for other, beside the contract.
- Read the agent's Python as tokens, never importing or running it.
- Report one of the 10 agreed names, other with a label from 9 known
frameworks, none for plain code, or unknown when it can't decide.
- Read the version only from the build's installed package files.
- End in unknown on a limit: 64 MiB read, 5,000 Python files, or a shared
work counter, with no clock, so the answer is the same every time.
- Keep detection failures away from the document and the run.
TH-8805
Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…804) Fixes from the dileepfix, callfix, depthfix and scriptfix review rounds and Dileep's 5 Nov review of #225: - Compose and alk.yaml load through a bounded YAML loader; the save's sibling check reads at most 128 KB and runs off the event loop. - Every Python parse the save reaches goes through python_source.py: 128 KB per file, 256 KB parsed and 64 KB tokenized per scan. - At the save the import walk reads at most 2 MB of scripts and tries at most 5,000 names; the browser check reads at most 16 MB of scripts. - Compose interpolation is charged per reference and capped at 64 KiB. - Every Compose service in the stack is checked for building a sibling. - The contract sent last in one turn is the one saved. Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…nd 1 (TH-8804) - Drop the NFKC fold of the import pre-filter; fullwidth names are a known limit. - Don't parse text holding a run of 32 combining marks, which the parser folds by insertion sort on 3.11 and 3.12.3; count parse and tokenize limits in bytes. - Look up each candidate once in the import walk, build package parents one folder at a time, and follow nothing more than 64 folders below the repository. - Check a shared Compose build once, charge each resolved path part to the selection budget, refuse paths over 4,096 characters, and compare containment by parts rather than 3.12's quadratic is_relative_to. - Refuse a non-string network_mode, ipc or pid, and environment values that aren't plain, before writing out an alias tree. - Scan the type-import strip at most twice; judge test folders below what the agent shares; skip import text inside script strings. - Copy at most 1 MiB of prompt lines and read at most 4 MiB of prompt files; write contract.json atomically; name the service in the sibling refusal and skip profile-gated services when no service is named; tell repair every service must build the folder; run the sandbox's credential walk off its loop. Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
3e4e3ec to
9c187fe
Compare
- Add --read-agent-only to the hosted authoring command: run only the agent reading and leave contract.json, agent.json, cost.json and any failure.json in --output, then stop. - Share one helper, reading_entry, with the agent-finding mode: job check, a soft limit and a hard timer, masking of secrets files, and a cost file that starts incomplete and is rewritten after each turn. - Count Claude's running totals once per turn, keep a contract the gate saved over any later fault, and refuse browser agents by the same rule a build uses. - Treat a turn limit as final, never retried: an error outcome, or ADK's whole call-limit message. A status-less provider failure is retried and marks the cost incomplete. TH-8951 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
- Count .js, .jsx, .mjs, .cjs, .mts and .cts files as other source, so a Python folder beside them reads unknown, not none. Test and example folders still don't count. - Answer unknown on any blocker before the chat-model rule, so a chat model names LangChain only when nothing unseen could build the agent. - Scan a repository-origin process in the source, the same base the runtime builds it from; bundle-origin processes stay in the bundle. TH-8805 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
- The chat-model-with-a-blocker test repeated test_blockers_stop_none, which puts a chat model beside every blocker; its check that the chat model is read as a model moves there. - The JavaScript test keeps only the test and example folder rows; the .js and .mjs rows repeat BLOCKER_CASES. - The standalone repository-process test goes; the folder-order row and the end-to-end hosted test cover the rule. TH-8805 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…up, and answer round 2 (TH-8804) - Charge each token the line it is read from, against a per-scan work budget, at both tokenize sites: 3.12.3 decodes a token's whole line again for each token. - Resolve Compose paths as realpath does, charging each lookup on disk, a link's target included; includes too. - Keep only found files and package lookups in the import walk's memo, and don't try a name over 4,096 characters. - Read an oversized prompt file once, counting what was read; honour COMPOSE_PROFILES in the profile skip. - Pin the Dockerfile memo key, the profile skip with a service named, the depth weighting, the prompt's read-once and the walk's line ends. - Stub the sandbox job in the live-stage test: the client's loop closing cancelled it mid-spawn, which hung about one run in six. Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Adds --find-agents to the authoring entrypoint, run through the shared reading helper. It writes /work/reading/agents.json in the shape the platform's AgentScanSerializer reads, plus cost.json on every return. - A plain code walk finds candidates (framework builds, langgraph.json graphs, agent.py, provider clients) and joins files into agents by their imports, without running anything. - Each agent's folder gets TH-8805's framework answer, with a build's limits, in one child process the scan's clock can stop. - A model is asked only when the walk finds nothing usable (mostly TypeScript and JavaScript repos). Its read/glob/grep tools stay inside the source, and only the paths it names, once checked, are kept. - Names, evidence and paths are built to fit the platform's rules, and the file is checked against a copy of them before it is written. - An agent at a path the platform's rule can't carry (a Next.js app/(chat)/ route, say) is left out with truncated set, and is reported as agent_path_unsupported when nothing else is found. TH-8950 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…t go at a budget stop Change review round 2 (A, Lows): a constant of 56 still passed the one-entry fan-out. A second shape, two entries over the same hubs, is dearer (about 59.5 bytes a file) and fails at 56. The venv check now counts every line that makes /opt/alk-venv, and the budget-stop test checks the facts are let go before the empty result is built. TH-9449 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…-review-followups Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
… and <!-- only in markup An opener that isn't a comment (a regex, a YAML value, a heredoc, a template literal) no longer hides the lines after it. A block is carried to later lines only when its closer is on a later line of the file, and <!-- runs on over lines only in markup files (.html, .htm, .xhtml, .xml, .svg, .vue, .svelte). Answers Dileep's P3 on #289 (TH-8953). Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…names out of comments Dileep's P3s on this PR: - With each subfolder charged once, a wide folder's own names, which os.walk holds while the walk is in it, were no longer covered by the old double charge: 50,200 empty folders charged 10.44 MB against a 12.59 MB peak. Each folder's listing is now charged once, when the walk reaches it, before any name is filtered out. A test of 30,000 empty folders holds the walk's peak under the charge. - The new tests' docstrings and comments no longer name the reviewer or the review; the reasons stay. TH-9449 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…uration Restricting the carry to a short list of markup suffixes let a model inside a real multi-line <!-- --> comment through in XML under other suffixes (.config, .csproj, .xaml) and in PHP or JSP. Now <!-- is a comment only to its line's end in known code languages and configuration, and runs on to its --> everywhere else. Adds tests for those files and for a comment after an earlier closed one. Review round 1 of the P3 fix on #289 (TH-8953). Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
env.xml and Dockerfile.xml took the configuration rule and accepted a model inside a real multi-line <!-- --> comment. Configuration is now decided by the suffix or the whole file name, so they carry <!-- again. Adds tests for the configuration half of the rule, by suffix and by name. Review round 2 of the P3 fix on #289 (TH-8953). Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…before the filter The one-reviewer check of the listing charge cleared, with two Mediums: - Building one tuple of every name held a slot a name while os.walk still held the listing, so 250,000 files charged 18.26 MB against a 19.06 MB peak. The two lists are summed apart. - Nothing held "before any name is filtered out". The wide-folder test now covers folders, files and links to a folder (listed as folders, then left out), and a charge moved after the filter fails it. Also: the first-pass test is computed once per folder, and two older test docstrings no longer name the reviewer. TH-9449 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
Review round 3 of the P3 fix on #289 (TH-8953). Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…nto feat/TH-9449-dileep-p3-followups Signed-off-by: khushalsonawat <khushalsonawat@gmail.com> # Conflicts: # tests/harness/test_scan_walk.py
…nto feat/TH-9448-no-debug-trace Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
[TH-9491] docs: answer Dileep's review of #278
fix(harness): keep a block comment open across lines, and save no prompt from unconfirmed lines
[TH-9448] fix: no traceback at any log level when the browser scan skips a file
Resolves two conflicts: the CI list keeps this branch's files, which already hold dev's sandbox-server lines; the sandbox server tests keep both sides' new tests. TH-8804 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…nto feat/TH-8804-source-subfolder Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…ings On CI a full garbage collection of the test process's heap, about 706,000 objects left by the tests before it, landed inside the save's parse and took 0.3 s, so the crafted-file test failed at 0.38-0.40 s against its 0.25 s line. The save's own hold was 0.05 s. The three loop-stall tests now collect, then freeze that heap while they measure, so the save's own collections still count and the earlier tests' don't. TH-8804 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…e earlier heap out of the model check's timings (TH-8804) sdk-smoke now runs tests/test_harness.py, test_contract_model.py and test_contract_writer.py. Six test_harness.py tests fail on dev 70fc43b the same way and are deselected by name. test_contract_model.py freezes the heap earlier tests built for each of its tests, so a full collection of it can't land inside a timed check. Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…e runner's clock (TH-8804) test_deep_flow_nesting_does_not_hold_up_the_save asserted the save took under 1 s, and failed once on a slow CI runner. It now counts the YAML loader's token fetches and checks the refusal comes at the sixteenth open group: 19 fetches with the limit, against about 19,000 for a sixth of the file without it. Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
| return [hit, *packages] | ||
| return [] | ||
|
|
||
| def script_module(path: Path, name: str) -> Path | None: |
There was a problem hiding this comment.
P3. Following up on the shared JS imports thread: relative files and index.* work now, but a package folder is still followed only through index.*. Its package.json main or exports is ignored.
Failure: agents/a/index.js does require('../../packages/shared'), and packages/shared/package.json has "main": "lib/llm.js". A key read in lib/llm.js is required on a whole-repo build and optional on a folder build, so the agent dies on its first call. The browser check also misses puppeteer required there. Known limits only cover names that need a workspace or path alias; this one is written as a path.
Fix: when the target is a folder, try its package.json main (and a string exports) before index.*, with tests for both and for the browser check.
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def _earlier_heap_frozen(): |
There was a problem hiding this comment.
P3. Freezing the heap helps, but the four took < 0.25 asserts (:2677, :2809, :2951, :3184) still race the runner, and CI now runs this file. Under load one of them failed 4 runs in 5 at 0.29 s. Use QUICK_SECONDS for those too.
| @@ -0,0 +1,382 @@ | |||
| """Read one agent the way a build reads it, and leave its description, with no build after it. | |||
There was a problem hiding this comment.
P2. The description covers the subfolder build and framework detection, but not the reading and scan work merged in through #245, #268, #269, #271 and #288: this module, agent_scan, scan_walk, scan_ai, scan_document, reading_entry, seed_contract, model_citation and the find-agents skill, about 8k lines of source and 15k of tests. None of those modules or their tickets appear in the body, and most of it reads as a log of review rounds. Please update it to list what the PR now ships, so whoever merges it into dev knows what's going in.
…me it took Dileep found the test still timed the cut, with enough slack that a cut 1.8 s late on a 2.0 s window passed, and that it couldn't tell the remaining window from the full one. The test now records the timeout passed to wait_for around the stalled emit_skipped_receipts and asserts 0 < timeout < window: the code passes the remaining window, which is always under the full one. The outer 30 s guard stays, so a hang still fails. The +1.8 plant and timeout=None both fail it. TH-9515 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
…ts it This PR now goes into #225's branch, where the reading took the difference of Claude's running totals itself (_own). The Claude backend now reports each turn's own figures, so the reading counted half: a two-turn reading recorded 0.07875 instead of 0.1575. The reading keeps each turn as reported; the two #225 tests that fed running totals feed per-turn figures; _gemini_priced reads current_rates, since the table is dated rows. Refs: TH-9057 Signed-off-by: khushalsonawat <khushalsonawat@gmail.com>
[TH-9515] test: prove the flush-window bound without timing the runner
Based on
dev. #224 (TH-8806) is merged intodev, anddevis merged into this branch (c07f4bb, a merge), so the diff shows only TH-8804. TH-8805 (#229) was merged into this branch and comes with it; see "TH-8805 is included" below.Summary
One repo can hold several agents. A job can now name one with
source.subfolder, and the kit treats that folder as the agent, with the whole repo still present so a project file above it can be used. The kit runs the agent from the folder and sends the value back asextension.source.subfolder. A folder it can't use fails withsubfolder_not_found(domainagent, stageacquiring_source, not retried) before any model call. With no folder, or an empty one, builds are byte-identical to today, except where the save's read limits below apply.Ticket: TH-8804.
Release order
HARNESS_SOURCE_SUBFOLDER_ENABLEDstays off until this is out:Known limits
_flatten_sourcesetsgeneric_harness_v1when a subfolder is sent, so the route's refusal no longer applies to platform jobs. That route sends no echo, and it returns 500 for every GitHub job ondev: TH-8978.data_store.schema_from,system_prompt_from,requirements_files. Not parsed: command forms such aspython -c,env -C../b,uv run -d../b,gunicorn --pythonpath=../b,.andenv PYTHONPATH=../b:..alk.yaml.devtoday: a separate ticket..git,.venv,node_modulesor the other folders no one's code is in. A Python file with no import in it doesn't use up the parse total. Past a budget the save's walk logs that it stopped and the credentials manifest says truncated; anything it didn't reach counts as not the agent's. Instrumentation is still recorded from what the walk reached, so a stopped walk can recordnone: TH-9509.sys.pathedits andimportlib, and JavaScript or TypeScript packages only a workspace or path alias can resolve. A uv workspace is followed as the build'suv sync --no-dev --packageinstalls it: the agent project's dependencies, all its extras and its default groups other thandev(withinclude-group), and, from each workspace member or in-repositorypathsource it reaches, what that one installs, with the extras asked of it, the root's sources applying to every member. At most 32 members are followed and 256 read, and 1 MiB ofpyproject.tomlin all; a**member pattern isn't expanded, and past a limit the walk says it stopped. A plan that asks for an extra a member declares makes the build sync every member, which the walk doesn't follow: TH-9513. A folder link is followed to its target inside the repository, charged a lookup for each part, and a path or link target past 128 parts below the root isn't; a link to a file isn't followed, a folder under an ignored name such asvendor/doesn't find its own keys, and a multi-lineimport typeis followed as if real. When the walk can't tell whose key something is, it says optional, so a missing key fails at the agent's first call. Nothing on the hosted path reads this list yet..pyis read in the encoding its coding line names, as Python reads it, except a codec that isn't text (such as rot13),punycode, whose decode costs the square of its size,idna, whose decode runs in Python for each label, andundefined. Those files are skipped by the scans (the browser-tool check still counts the browser packages and tool markers they name, as whole names in their bytes, since Python runs them; feat(harness): refuse browser, computer-use and crawler agents #224 does the same). Aunicode-escapefile is decoded as Python decodes it, so an import spelled in escapes counts, up to 4 MiB per scan; past that it is searched as bytes. Three rarer codecs can still spell an import in bytes the scans don't see (raw_unicode_escape,utf-7,iso2022_jp; only deliberate hiding reaches it): TH-9490. A non-ASCII byte on the coding line is read as Python reads it. A first line that holds more than 64 places that look like a cookie but aren't, before the real one, is read as a refused codec's file is. The import walk reads all of these,unicode-escapetoo, as UTF-8, as it does a file its codec can't decode (which Python refuses too): decoding with replacement runs Python's own error handler for each byte in most codecs. So a crafted file Python reads inidnaorpunycodecan give the walk fewer imports than Python finds. Python's own refusals of a non-UTF-8 byte on the coding line's own line, and of utf-16 or utf-32 named on it, aren't copied: such a file is read anyway.playwrightortraceai_openai, which Python reads as the plain name, isn't found by the save's checks. A.pyholding a run of 32 combining marks isn't parsed, and a${}inside a JavaScript template literal isn't followed.infra/service the stack runs, is refused as building another agent's folder; it can be pulled as an image instead.pyproject.tomlnaming a provider can require its keys on an agent that doesn't use it.Differences from the approved design
runtime.source_subfolder) on subfolder builds only, so it also appears insideextension.contract.subfolder_not_found, naming both.compose.yamlis allowed only when every service it runs builds the folder or above it. With no service named, a service behind a profile isn't checked unlessCOMPOSE_PROFILESasks for one of its profiles, since nothing else starts it.alk.yamlrefuse YAML of more than 200,000 nodes, counted across every included file (an alias repeats a node without counting it, sonetwork_mode,ipcandpidmust be strings and environment values plain, as Compose takes them),<<merges nested more than 32 deep,[or{nested more than 16 deep, a%TAGprefix over 1,024 characters, a string that interpolation would build past 64 KiB in all, counting what it keeps and what it adds (compose_structural_interpolation_limit), too many lookups on disk to resolve the stack's build, include, env-file and mount paths, a link's target counted, and each empty,.or..part walked (the same code), a path over 4,096 characters (compose_source_path_invalid,compose_include_path_invalid), and a base-60 number (1:2:3) of more than 17 parts, whose cost grows with the square of its length. At a Compose file the code names the reason (compose_yaml_too_large,compose_yaml_too_deep,compose_yaml_tag_too_long, …); atalk.yamlit followsruntime_manifest_invalid:.alk.yamlover 1 MiB is refused, read only that far..pyheld the conversation 13.5 s per parse: a folder save took 55 s, and a save without a folder 40 s, as on base (41 s). Now a.pyis parsed only if it's a regular file of at most 128 KB. Each scan parses at most 256 KB, and tokenizes at most 64 KB, counted in bytes; each token is also charged the line it's read from, since 3.12.3, the sandbox's Python, decodes the line again for each token (one 64 KB line of short tokens took 3.8 s). A file holding a run of 32 combining marks isn't parsed: on 3.11 and 3.12.3 the parser takes seconds to fold such a name. The instrumentation and browser-tool scans parse only files that name something they look for. On 40 crafted 128 KB files, a save takes about 1.6 s on 3.12 (4.0 s on 3.11), and the conversation pauses at most 0.27 s (0.8 s on 3.11). Imports in a.pyover 128 KB, or past a scan's total, don't count, and past the tokenize total the browser-tool scan doesn't read a file's markers. At the save, the import walk matches at most 2 MB of scripts and tries at most 5,000 import names, and with or without a save it follows nothing more than 64 folders below the repository, looks up each package and each file it finds once, keeps no other misses, and doesn't try a name over 4,096 characters; before, a crafted TypeScript file held the conversation about 20 s, and a chain of 358 nested packages made a save wait a minute. The browser-tool check reads at most 16 MB of scripts, with or without a folder; before, 128 MB of crafted scripts made a save wait 14.6 s (base code), and in a monorepo saved without a folder, scripts past 16 MB now aren't checked for browser tools. Before, a link to/dev/zeronamed.pyheld every save (base code).alk.yamland Compose files together (the largest real Compose file seen is 53 KB), and skips a runtime with Compose overlays, which rundocker compose. Past either, the check is left to bundling, which refuses the same service, as with no folder.failure.jsonleaves the bundle cache alone.subfolder_not_found, though the contract has the format checked on the platform only, because the kit also runs from jobs the platform never checked.extension.source.subfoldercomes from the folder the bundle recorded. If that differs from the job's, no document is sent.TH-8805 is included (merged in from #229)
#229 (TH-8805) was merged into this branch on 4 Nov, so this PR also brings
src/fi/alk/harness/framework_detection.py(1,027 lines after the fixes) and its tests todev. The wiring for it is inhosted_entrypoint.py,scenario_source.pyandproviders/agent.py. Please review it as part of this PR. #229's own description has the full detail: #229What it adds. Every build's agent document says which framework the agent uses:
extension.framework = {name, version}, pluslabelforother. The answer comes only from reading files. The customer's code is never imported or run, and no model is asked. The words are the Studio contract's 10, plusother,unknownandnone.How it was verified:
livekiton 3.11, and one istest_grade's cap test;Your review's three framework comments are fixed in #248 (#248), into this branch:
.js,.jsx,.mjs,.cjs,.mtsand.ctsnow count as other source. So a Python folder with them, outside test, example and similar folders, readsunknownunless a framework is named. File names are matched case-sensitively, as before, soINDEX.JSstill isn't counted;unknownbefore the chat-model rule;Known limits:
extension.frameworkyet (platform #3661).unknownis answered whenever the check isn't sure.from livekit import *read asnone.version: null.unknown.unknownwhen anything blocks. That includes test or eval files that import a framework, its ownagentspackage, and JavaScript or TypeScript outside test and example folders, including config and built files such astailwind.config.js,static/js/andweb/dist/.setup_hashincludesextension.framework, so an agent whose answer changes gets a new hash on its next build.Landing with TH-8806 and TH-8805
#224 is merged into
dev(dc25cad), anddevis merged into this branch at c07f4bb, not rebased. This branch uses #224'sEXIT_REFUSEDandfailure_file.py.submit_contract's browser-tool scan reads the agent's folder, what it imports and the files directly above it, not sibling folders, with a missing folder counting as nothing found. TH-8805 (#229) was merged into this branch on 4 Nov; its fixes for your review are in #248, into this branch.Landing with #274 (TH-9057, TH-9039: Claude's per-turn cost)
#274 changes
backends/claude.pyto report each turn's cost, and the shape of the price table. Whichever of #274 and this PR lands second updatestests/harness/test_agent_reading.py:_ReadingStage._ownand therunning_totalsflag go, and three tests move to per-turn figures from the fake backend:test_no_contract_after_the_reminder_is_incomplete,test_an_error_flag_without_a_status_still_gets_the_reminderandtest_claude_running_totals_are_counted_once._gemini_pricedchanges torate_in, rate_out = current_rates(_PRICED_GEMINI). Two tests use it:test_a_gemini_call_limit_is_incomplete_with_no_reminderandtest_a_gemini_call_limit_after_a_malformed_worker_reply_gets_no_reminder.complete: falseonly for a model missing from the price table.test_a_claude_reading_adds_per_turn_figures_as_they_comefails while_ownis still here after the merge.Verification
uv run ruff check .: not run on the whole repo.ruff checkon every changed.pyfile passes.uv run pytest -q: not run, because of the laptop limit. CI runs the list insdk-smoke.yml, which now includes this change's test files excepttests/test_harness.py. One file runs with 2 tests deselected, which fail ondevtoo: TH-8978. The nine intest_harness_architecture.pyrun again since test(harness): fix the nine hosted-GitHub tests and guard what their fixes left unchecked #272 (TH-9444).agent-learn release-proof: not run.[nested hundreds deep, about 4 s under the save's 128 KB), also High. He asked for a new count after it. All five are fixed here, and so is analk.yamlread that trusted the file's reported size. The new count's first round found that crafted files still held the save about 1.2 s and that a%TAGprefix could take 1.5 GB, plus an older endless read of a linked.py; so the save's checks now run off the event loop, and both are fixed. Its second round found that Python's parser still held the conversation, 13.5 s on one crafted.py(on base too), that a value fromalk.yamlrepeated in Compose could take a save past 1.5 GB, and that of two contracts sent together the first could win. All three are fixed, with the smaller findings. Its third round found a crafted.tsfile holding the conversation about 20 s and a Compose wait of about 4 s; both are fixed, and Khushal accepted the Python ceiling (1.6 s on 3.12, 4 s on 3.11) and asked for a new count. That count's first round found five more ways crafted files could stall the save: a Unicode fold on non-ASCII files, the parser folding crafted names on 3.11 and 3.12.3, a chain of nested packages, deep build paths checked on every service, and an alias tree innetwork_mode. All five are fixed here, with its Mediums and Lows; on the reviewers' own files every save took at most 0.4 s. Its second round found two more: one long line of short tokens held the tokenizer 3.8 s on 3.12.3 (on base too), and a link to a deep folder made 4,500 builds take 9.7 s, since a lookup through a link wasn't counted. Both are fixed: each token is charged its line, and resolving charges each lookup on disk it makes. So is the walk's memory at bundling's limits (1.5 GB at 20,000 names, now under 300 MB), with its other Mediums and Lows, and a sandbox test that the first round's off-loop credential walk made hang about one run in six, which now stubs its job. Its third round, the last, found one more: link targets padded with empty parts, which the resolver walked uncharged (5.25 s on 3.12.3). Khushal chose to fix it and allow one more round on that commit: each empty,.or..part is now charged, and the save takes 0.29 s. Its six smaller findings went to a follow-up ticket, since folded into TH-9445. The fourth round, on that commit and on the merge of feat(harness): refuse browser, computer-use and crawler agents #224's latest head, found the merge faithful and two more: links that grow the path as they unwind (4.07 s), and the browser scan reading each folder above the agent's install records with a fresh 64 MiB budget, a path object per row (5.74 s, 718 MB). Khushal chose to fix both here, with its five smaller findings, and one more round on that commit. A path past twice the system's limit is refused (0.07 s now), and the folders at and above the agent share one 16 MiB record budget, rows kept as text (0.13 s now; 0.64 s 8 folders deep). Coding lines are now honoured by the import walk too, and with lone CR line ends. The fifth round, on that commit, found three more, all from it: apunycodecoding line made the import walk's decode cost the square of the file's size (9.4 s), anidnaone made the walk raise and every save fail, and matching every prefix of a path against the records added 1.8 s. Khushal chose to fix them and allow one more round on that commit:punycodeandundefinedaren't read, the walk falls back to UTF-8 when a codec can't replace what it can't read, and a record's row is matched only against the file itself. The import walk still charged a name once however deep it went; that went to a follow-up ticket, TH-9442. The sixth round, on that commit, found two more coding-line stalls, both older than it:idnawith valid labels (3.6 s, from the merge of feat(harness): refuse browser, computer-use and crawler agents #224), and decoding with replacement, which runs Python's own error handler for each byte in most codecs (2.1 s). It found that reading the root's records first hadn't fixed the fifth round's Medium, and that TH-9442's shape takes 12.3 s, not 2.4 s. Khushal chose to fix all of it here, TH-9442 and the five smaller findings included, and allow one more round.idnaisn't read, and the walk decodes strictly before falling back to UTF-8 (both 0.1-0.25 s now). Once the record budget is spent, a browser package counts as installed, which leads to a second look, not a refusal. A file is checked against the nearest four folders' records. At the save, the walk stops at 100,000 lookups on disk, a file found charged for each part of its path; honest folders use under 9,000. On TH-9442's 12.3 s shape the walk takes 1.4 s, and the save 4.9 s. The rest is the folder's 31,000 files, which a whole-repository save on feat(harness): refuse browser, computer-use and crawler agents #224 takes as long over. Hawk also found, reviewing a later plan, that the save resolved paths the contract names without charging the lookups: a crafted prompt path took 2.1 s, any number of them could be named, and the runtime's paths and tool modules were resolved the same way. Khushal chose to fix it here as a second commit, which joins the seventh round: one budget of 20,000 lookups covers every such path in a save (honest contracts use under 900), past it the save says so, and a path no system can open is answered rather than raised. Dileep's review of 10 Nov is answered in a third commit, which also joins the seventh round: credentials for a uv workspace member, instrumentation that follows the folder's imports, bind mounts and env files checked like builds, a folder that links to its own parent refused, a narrower catch with a logger in the sibling check, and the import walk saying when a limit stopped it. Replacing the per-scan budgets with one subprocess and a deadline is TH-9445. The seventh round (Owl) found no regression in either commit, and three more crafted stalls: imports through committed folder links escaping the walk's lookup budget (11.6 s),unicode-escapefiles (7 s), and a multi-megabyte path in the save's resolver (7.4 s). All three are fixed in a fifth commit: at the save an import through a folder link isn't followed,unicode-escapeisn't decoded, and a path past twice the system's limit is refused first. Its smaller items and the many-files cost are folded into TH-9445. PR 224's last heads are merged in, and Dileep's new point on it, that a file left undecoded could hide a browser import, is answered the same way here. Under Khushal's 10 Nov rule the developer runs the cold reviews: three fresh reviewers spot-checked the fourth and fifth commits and the merges of feat(harness): refuse browser, computer-use and crawler agents #224. They found a deep script import that crashed the save (the link check now runs after the depth cut), a merge stall of 9.2 s (the raw searches now run under a 1 MiB total), an import spelled inunicode-escapeescapes (decoded as Python does under a 4 MiB total, the rule both PRs now share), and a non-ASCII coding line that hid a file. One more fresh reviewer checked those fixes and found an 18.6 s stall the coding-line fix had added, now 1.9 s, and a CRLF case; both are fixed. The rest is ticketed: TH-9448 (the cost of files left undecoded) and TH-9490 (three rarer codecs).Dileep's third review (10 Nov, four comments) is answered here, with six self-run cold review rounds of two fresh reviewers each, under Khushal's plain Lane N rule. The first commit makes a workspace member the agent depends on part of the walk (Khushal: "fix it now", in place of the known limit), follows committed folder links at the save, charged, builds instrumentation's reach from the whole walk, and guards the 3.13 assertion. Rounds 1 to 4 each found crafted save stalls in those lines, from 1.8 s to 53 s: pyproject files parsed uncharged, member globs through links, every name tried against every member, link targets walked uncharged, a coding line's cookie pattern over a 128 KB line, eight escape lookbehinds at every byte, a long uv
pathsource, the sources table rebuilt per requirement, a member re-read for each new extra, and the folder walk searching back down to a missing folder for each candidate. Rounds 1 and 2 also found that every member, a sibling agent's included, counted as the agent's code; the walk now follows only what the build installs, compared withuvitself. Each was fixed and re-measured, at 0.06 to 1.4 s on 3.12.3. After round 4, Khushal set the line for this PR at 5 s: a crafted stall these lines bound but don't remove is a TH-9445 item below it. Round 5 found two above it (a cookie search re-reading a line, 8.5 s, and the extras regex costing the square of a requirement, 51 s), both fixed. A design review of TH-9490 on this branch (Pine) found that the cookie cap refused a file whose real cookie came first, and that plain files ahead of the agent's imports could spend the parse total and hide a shared import with no log; both are fixed. Round 6 cleared, with nothing above Low; its six Lows (tests and two small simplifications) are fixed in the last commit, which had no round, as the rule allows. The merge of #272 (TH-9444, merged into this branch at c1685ec) brings onlytests/test_harness_architecture.py, andsdk-smoke.ymlruns its nine tests again. Tickets from these rounds: TH-9509 (a stopped walk records instrumentationnone), TH-9513 (--all-packagesagainst the walk), and the folder count, now part of TH-9445.Dileep's review of 11 Nov is answered in 1132192, with two rounds of one fresh reviewer each, under the leaner rule for a small change. The first round wasn't clear; the second cleared, and its two code Lows are fixed with no further round.
.pylink still counts as unread: checking it would follow its chain uncharged, and some targets raise..pyholdingimportthat doesn't parse counts as unread, whatever the reason.Merges with
dev:src/fi/alk/harness/sandbox_server.pyimports bothdev'sSecretResolutionErrorand this branch's folder helpers, andtests/test_harness_sandbox_server.pykeeps both sides' new tests.test_public_github_branch_url_populates_job_refandtest_private_github_submission_becomes_hosted_jobfail ondevalone (github_commit_sha_required, job.py:413). CI deselects them (TH-8978).CI's list, run on a Mac with CI's install (3.12.14), at cde91b3: 4738 passed, 3 skipped, 2 deselected, 1 xfailed. The changed-area files on 3.12.3, on a Mac: 2243 passed.
test_certify_only.py's real seed test (3 cases). It needsALK_TEST_POSTGRES_DSN.test_hosted_github_route_accepts_a_submitted_value. Submit doesn't yet give hosted jobs thededicated_vmruntime.Since then:
devis merged in again.sdk-smoke.ymlkeeps this branch's list, which already holdsdev's three sandbox-server lines, andtests/test_harness_sandbox_server.pykeeps both sides' new tests.test_a_crafted_python_file_does_not_hold_up_the_conversationfailed at 0.38-0.40 s against its 0.25 s line.Fix-only checks after the last clear reviews
Each commit made after a review round had cleared got a fix-only check by one fresh reviewer, and each was CLEAR with no findings:
cf3ebb4, round 6's six Lows: each fix's test fails on its plant.
The part of 1132192 after its second fix-review round cleared:
f8a960d, the restored
SecretResolutionErrorimport. Nothing else that merge touched is missing.The merges 623d7de, cde91b3, ababd8b and 4ec0bfc. Each was recomputed and compared. Every hand resolution keeps both sides, and no test, CI-list entry, import or code from either side is lost.
c0e51b7, the GC freeze in the loop-stall tests.
This commit's sdk-smoke additions. CI now runs every test file this PR touches:
tests/test_harness.py,test_contract_model.pyandtest_contract_writer.py;tests/providers.The first check wasn't clear on one point:
test_contract_model.py's timing checks had no guard against a garbage collection of the earlier tests' heap. Its fix freezes that heap for each test in the file, and a second fix-only check covered it. The two checks nearest their 0.5 s line, at about 0.27 s, are:test_a_crafted_file_of_block_comments_does_not_hold_up_the_check[a-file-of-openers];test_a_quote_that_grows_when_normalised_costs_little. It runs undertracemalloc, and once read 0.477 s on a loaded machine. If it fails on CI, it is deselected by name.Six
tests/test_harness.pytests are deselected, because they fail ondev(70fc43b) the same way. They're named in the workflow:test_hosted_claude_session_uses_durable_resume_and_restricted_tools;test_a_skill_only_names_tools_its_stage_actually_has;test_connect_only_provider_world_cannot_create_shadow_state;test_the_system_prompt_stays_within_budget;test_a_save_refused_for_the_suites_shape_is_accepted_on_the_second_try;test_dropping_every_scenario_needs_an_explicit_everything.The deep-flow test reads no clock now.
test_deep_flow_nesting_does_not_hold_up_the_saveasserted the save took under 1 s, and failed once on a slow runner (test(harness): a Deep Agents reference agent for the hosted path (TH-8810) #306's run 43166681740). It now counts the YAML loader's token fetches, under 100, and checks that the refusal comes at the sixteenth open group.Checklist
agent_learning.*,agent-learn, or@future-agi/agent-learning-kitRefs: TH-8804