feat(cli): preview external tool invocations and document the external-tool workflow - #582
devin-ai-integration[bot] wants to merge 48 commits into
Conversation
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
Co-Authored-By: jason.han <hanhuijun@gmail.com>
… feature/tool-dry-run-and-demo Co-Authored-By: jason.han <hanhuijun@gmail.com>
…un-and-demo Co-Authored-By: jason.han <hanhuijun@gmail.com>
…un-and-demo Co-Authored-By: jason.han <hanhuijun@gmail.com>
…p failed calls Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
There was a problem hiding this comment.
Note
Newer findings are available below. Devin Review posted a newer report on this PR, in addition to the findings presented here.
Devin Review found 6 new potential issues.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| } else { | ||
| lines = append(lines, "env:") | ||
| for _, pair := range d.Env { | ||
| lines = append(lines, " "+pair) |
There was a problem hiding this comment.
There was a problem hiding this comment.
Intended: the preview's purpose is to show exactly what the process would be started with, environment included. The only values that can appear there are the manifest's own env entries and variables the operator explicitly listed in OPENSYSML_TOOL_ENV_PASSTHROUGH from their own process environment — nothing the model or another party supplies — and the preview prints to the operator's terminal, not to a file or record.
| text := head + ": " + u.Executable | ||
| if u.Args == nil { | ||
| text += " < " + string(u.Stdin) | ||
| } else { | ||
| for _, arg := range u.Args { | ||
| if arg == "" || strings.ContainsAny(arg, " \t\r\n\"'") { | ||
| arg = strconv.Quote(arg) | ||
| } | ||
| text += " " + arg | ||
| } | ||
| } |
There was a problem hiding this comment.
Intended: the record already stores every input value the run was made with (inputs), and the argv / request the tool was given is composed from those same values — that is the provenance the tools attribute exists to keep, so a reader can reproduce the run. A value that must not be written into a record must not be an input of a recorded run; -record-run is an explicit, opt-in act by the modeller.
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…handling Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…un-and-demo Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # internal/workspace/libs/stdlib.snapshot
…and-demo Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # internal/exec/analysis/record/record_test.go
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…o feature/tool-dry-run-and-demo Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
… does Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…an exploring schedule Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ol provenance clear of the conclusion Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…concluded Co-Authored-By: jason.han <hanhuijun@gmail.com>
…un-and-demo Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # client/rust/conformance/sysml.descriptor.binpb # internal/exec/analysis/result.go # internal/exec/analysis/tool.go # internal/workspace/libs/stdlib.snapshot
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…run's failure ends Each tool engine reports the call it ran to the asking runner directly, so a run the all plan drops behind an earlier fault still joins the provenance; a dry run reached before another failure reports an unresolved verdict with the preview. Co-Authored-By: jason.han <hanhuijun@gmail.com>
… of it Under all every covering engine runs, so a non-tool engine earlier in name order does not keep the tool from running; the dry runner passes it instead of leaving the preview undecided as it does under auto. Co-Authored-By: jason.han <hanhuijun@gmail.com>
… tool cover A built-in refusing the call never runs and one covering it runs beside the tool, so both are passed; an external engine cannot be probed and leaves the preview undecided, as does a covering built-in when no entry of the tool's name exists. Co-Authored-By: jason.han <hanhuijun@gmail.com>
Under all an external engine runs beside the tool or not at all; it does not displace it. Only without an entry of the tool's name does it, as one that may answer the call, leave the preview undecided. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…un-and-demo Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # client/rust/conformance/sysml.descriptor.binpb
There was a problem hiding this comment.
Devin Review found 1 new potential issue.
1 flag not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
| exec, err := ctx.CreateActionExecutorWithInputs(sym, self, inputs) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to create executor: %w", err) | ||
| } | ||
| defer exec.Release() | ||
| exec.SetTrace(s.trace) | ||
| return exec.RunToCompletion() |
There was a problem hiding this comment.
🟡 Preview bypasses an object's existing action
When an object already performs an action, runActionToCompletion starts another performance. performanceOf instead joins the existing one or rejects explicit inputs, so the preview can describe a call the run cannot make.
Learn more
An object can already have an action performance attached to it. The ordinary action path uses performanceOf to find and continue that performance, and rejects new arguments when it exists. The preview creates a fresh executor instead, so it can preview an action with different inputs or from a different point in its execution.
Example: An object already performs Heating with mass 12.5. Previewing Heating(30) shows mass 30, although the ordinary action path refuses those explicit inputs.
Recommended fix: Make the action preview use the same existing-performance selection and input checks as the ordinary action path, while restoring the context snapshot afterward.
Was this helpful? React with 👍 or 👎 to provide feedback.
What and why
Builds on #573 (
invocationblock) and #577 (replyblock) and must merge after them; the branch is cut fromfeature/tool-reply-formats, so until those land the diff shows all three. Once they merge this PR will be rebased ontodevelopso only its delta shows.With a manifest able to compose argv/env/stdin from model values and parse arbitrary replies, an author needs to see what a tool would be handed before trusting a run, and a recorded run needs to say which program produced it. This PR adds those surfaces, the documentation of the whole workflow, and a worked example.
sysml -tool-dry-run <case|action>and%tool <case|action>[(<args>)] [<object>]perform the target in the session's own context under aToolRunnerthat, at the first tool call reached, composes the invocation from the resolved manifest entry and fails the performance with a typedanalysis.ToolDryRunErrorthe surface swallows — nothing after the call runs and no process starts. The performance runs on aruntime.Snapshotof the session's context that is restored afterwards, so the preview sees the session's current values and leaves no trace of what it performed. TheDryRunnerremembers the first preview it made, so a call reached only while an objective or assertion is evaluated is still reported; an exploring schedule is refused (ExplorePreviewError), since a preview shows one run's first call. The session's own tool runner is put back after the preview. Monte Carlo row records bound their tool calls at a mark taken between settling the runs and concluding the case (SettleMonteCarloRuns/MonteCarloRun.Conclude), so the conclusion's calls appear only on the sample record. The preview lists manifest, executable, protocol, each argv element as%q, the environment (inherited from this processfor an entry without aninvocationblock), cwd, the stdin mode and its rendered bytes, the input file's bytes, the inputs and outputs of the call, and thereplymapping per output (path / column / row / unit selectors, type).{inputFile}and{outputDir}render as<inputFile>and<outputDir>since no directory is made.composeis split intocallScope/InputFile.inputData/renderso the preview and the real invocation share one rendering path.no ToolExecution-annotated action was reached; nothing to preview. A case with several tool actions shows the first (the later ones depend on outputs the dry run does not have).-sweeprefuses-tool-dry-run; the action path usesCreateActionExecutorFor+RunToCompletiondirectly so no%actiondebugger session is left behind.analysis.Result.Tool(ToolUse: tool, version, manifest file, executable, argv — or the request read on stdin for anobjectentry) is filled by the tool engine; the plan'stoolRunneraccumulates them, each attributed to the runtime context that made the call, andPlan.Tools/Plan.ToolTexts()/Plan.ToolTextsIn(ctx)expose them in call order. A call that failed after its process was started (spawn, reply read, binding) is kept too: the engine returns aToolUseErrorwrapping the failure, and itsToolUse.Failedrenders as… failed: <message>.record.Run.Toolswrites each run's own calls into its record astools = ("ThermalSolver 2.3 from /etc/opensysml/tools/thermal.json: /usr/bin/python3 solve.py --mass 12.5", …);, andAnalysisRecords::RecordedRungainsattribute tools : String[0..*];. Records of runs that reached no tool are byte-for-byte unchanged (existing goldens untouched; a newtool_run.sysml.goldencovers the attribute). Sweep rows and Monte Carlo runs each list only the calls their own row made; the whole-sample record lists them all.ListEnginesand in the PythonEngine.protocol; the proto comment and the Python docstring now describe the tool spellings (object,argv+<stdin>/<reply>),Engine.explain()shows kind and protocol for tools as it does for engines, with a Python client test and a gRPC test assertingargv+none/csvfor a composed entry.docs/manual/running-external-programs.md("Running external programs from a model") walks manifest → model → run → record → document and is linked from the analysis and recording pages and from the External tools section ofdocs/reference/environment.md, which coversOPENSYSML_TOOLS,OPENSYSML_TOOL_TIMEOUT,OPENSYSML_TOOL_MAX_OUTPUT,OPENSYSML_TOOL_ENV_PASSTHROUGHandOPENSYSML_TOOL_KEEP.-tool-dry-run/%toolare in the CLI and REPL references and the regenerated manual page.examples/external-tool-demo/: a stdlib-only Python script taking--mass --power --ambientand answering CSV, its manifest (executable: python3looked up onPATH,cwd: "."so the script sits beside the manifest), a model whoseSolveaction carriesToolExecution, aRecordspackage and a document tabulating the recorded runs, a README with the exact commands (the manifest directory must live outside every workspace, so the README copiestools/out of the checkout), and the renderedreport.md.TestExternalToolDemoincmd/sysmlruns dry run →-record-run→ Markdown render end to end and skips whenpython3is absent.Known limitation: a document
Projectreads a record's own features, not its annotation's attributes, sotoolsis read in the record's source rather than tabulated (same asrunAt/commandtoday).How it was verified
internal/frontend/repl/tool_test.go:%toolhappy path over aninvocation+replyentry (argv, env, stdin, input file,<outputDir>, reply lines, the tool did not run), object-protocol entry, no manifest, unsent input, action target leaving no debugger session, the session's values, objects and trace left as the preview found them, an action's positional and named arguments bound before the preview (ctx.ActionInputs), usage and%help.cmd/sysml/tool_dry_run_test.go:-tool-dry-runhappy path, no manifest, unsent input;cmd/sysml/external_tool_demo_test.go: the demo end to end.internal/exec/analysis/record: newtool_run.sysml.golden; repl record test assertingtools = (…)for a tool-computed case;internal/frontend/grpc/engines_test.goprotocol assertion;client/python/tests/test_engines.py.go build ./...,go vet ./...,gofmt -l .,make lint,go test -race ./..., the training and pilot corpus gates with both require variables,make man-check,make docs-check,make docs-counts,make stdlib-snapshot-check,python -m mkdocs build --strict,python3 scripts/check-doc-links.py,python3 scripts/changelog.py check,pytest client/python/tests.corpus_roundtrip_expected.txtandapi_json_roundtrip_expected.txtgain onestablerow each; the pilot-differential baseline and doc-count blocks were regenerated for the 45th example.Checklist
make testandmake lintpass locallychanges/unreleased/<slug>.<section>.md, not as an edit toCHANGELOG.mdmake docs-countsrun if a gate count moved (compliance rows need nothing: the census is counted at docs build)F4,K5) in the body, docs, or changelog