sigit-code 1.5.11__tar.gz → 1.5.13__tar.gz
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.
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.agents/AGENTS.md +8 -0
- sigit_code-1.5.13/.cargo/config.toml +7 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.github/workflows/ci.yml +5 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.github/workflows/release-github.yml +5 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.gitignore +1 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/AGENTS.md +8 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/CHANGELOG.md +34 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/CLAUDE.md +8 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/Cargo.lock +1 -1
- {sigit_code-1.5.11 → sigit_code-1.5.13}/Cargo.toml +1 -1
- {sigit_code-1.5.11 → sigit_code-1.5.13}/PKG-INFO +1 -1
- sigit_code-1.5.13/packaging/windows/check-static-crt.ps1 +35 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/packaging/winget/getSigit.siGitCode.installer.yaml.in +2 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/main.rs +47 -2
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/provider.rs +16 -1
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/tools.rs +200 -7
- {sigit_code-1.5.11 → sigit_code-1.5.13}/tests/acp_endpoint_errors.rs +3 -1
- {sigit_code-1.5.11 → sigit_code-1.5.13}/tests/acp_permissions.rs +111 -4
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.agents/skills/agent-client-protocol/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.agents/skills/ai-assisted-coding/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.agents/skills/branding/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.agents/skills/run-sigit/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.agents/skills/run-sigit/driver.mjs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.agents/skills/run-sigit/tui-smoke.sh +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.agents/skills/sigit-code-release/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.agents/skills/tool-calling/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.claude/skills/agent-client-protocol/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.claude/skills/ai-assisted-coding/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.claude/skills/branding/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.claude/skills/run-sigit/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.claude/skills/run-sigit/driver.mjs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.claude/skills/run-sigit/tui-smoke.sh +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.claude/skills/sigit-code-release/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.claude/skills/tool-calling/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.github/workflows/release-aur.yml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.github/workflows/release-crates.yml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.github/workflows/release-homebrew.yml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.github/workflows/release-npm.yml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.github/workflows/release-nuget.yml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.github/workflows/release-pypi.yml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.github/workflows/release-scoop.yml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.github/workflows/release-winget.yml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/.nvmrc +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/LICENSE +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/README.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/docs/headless.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/docs/hooks.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/docs/mcp.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/examples/settings-with-hooks.toml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/examples/skills/README.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/examples/skills/commit-message/SKILL.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/npm/README.md.tmpl +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/npm/package-compat.json.tmpl +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/npm/package-main.json.tmpl +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/npm/package.json.tmpl +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/npm/scripts/render-main-package.cjs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/npm/scripts/render-platform-package.cjs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/npm/sigit/.gitignore +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/npm/sigit/README.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/npm/sigit/package.json +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/npm/sigit/src/index.ts +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/npm/sigit/tsconfig.json +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/nuget/.gitignore +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/nuget/sigit/Program.cs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/nuget/sigit/README.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/nuget/sigit/SiGit.Code.csproj +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/packaging/aur/PKGBUILD.in +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/packaging/nfpm.yaml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/packaging/winget/getSigit.siGitCode.locale.en-US.yaml.in +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/packaging/winget/getSigit.siGitCode.yaml.in +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/pypi/README.md +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/pypi/pyproject.toml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/pyproject.toml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/rust-toolchain.toml +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/account.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/backend.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/browser_auth.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/chat.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/commands.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/credentials.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/frontmatter.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/headless.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/hooks.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/inline_tool_calls.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/instructions.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/mcp.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/models.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/permissions.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/session_store.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/settings.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/setup.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/skills.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/subagents.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/src/workspace.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/tests/acp_multi_root.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/tests/acp_session_list.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/tests/acp_session_load.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/tests/acp_tool_stdin.rs +0 -0
- {sigit_code-1.5.11 → sigit_code-1.5.13}/tests/headless_mode.rs +0 -0
|
@@ -351,6 +351,14 @@ GitHub release, so `release-github` builds the binaries, attaches the assets, an
|
|
|
351
351
|
is logged as a warning rather than failing the others, so an unconfigured channel does not block a
|
|
352
352
|
release. Their inputs live in `packaging/`.
|
|
353
353
|
|
|
354
|
+
The Windows binaries link the MSVC C runtime statically (`+crt-static` in `.cargo/config.toml`).
|
|
355
|
+
winget, Scoop, npm, PyPI and NuGet all ship the bare exe with no installer, so a dynamic build
|
|
356
|
+
that imports `VCRUNTIME140.dll` fails to load on a machine without the VC++ Redistributable
|
|
357
|
+
(`0xC0000135`, which is what failed winget's first validation).
|
|
358
|
+
`packaging/windows/check-static-crt.ps1` runs in CI and in `release-github` and fails the build if
|
|
359
|
+
a Windows exe imports the runtime again. Don't set a `RUSTFLAGS` env var in those workflows: it
|
|
360
|
+
replaces the config rustflags instead of adding to them.
|
|
361
|
+
|
|
354
362
|
Every release asset now carries a `.sha256` sidecar, not just the macOS Homebrew tarball. Scoop,
|
|
355
363
|
winget, and the AUR PKGBUILD each need one, and they consume the raw binaries rather than the
|
|
356
364
|
tarball. `release-github` also builds a `.deb` and `.rpm` per Linux target with nfpm
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Link the MSVC C runtime statically on Windows. Without this the binary
|
|
2
|
+
# imports VCRUNTIME140.dll and the UCRT api-ms-win-crt-*.dll set, and on a
|
|
3
|
+
# machine without the Visual C++ Redistributable it dies at load time with
|
|
4
|
+
# STATUS_DLL_NOT_FOUND (0xC0000135) before main runs. winget ships sigit as a
|
|
5
|
+
# portable exe, so there is no installer to bring the runtime along.
|
|
6
|
+
[target.'cfg(all(windows, target_env = "msvc"))']
|
|
7
|
+
rustflags = ["-C", "target-feature=+crt-static"]
|
|
@@ -119,3 +119,8 @@ jobs:
|
|
|
119
119
|
|
|
120
120
|
- name: Test
|
|
121
121
|
run: cargo test --locked --target ${{ matrix.target }}
|
|
122
|
+
|
|
123
|
+
- name: Check Windows binary has no VC++ runtime dependency
|
|
124
|
+
if: runner.os == 'Windows'
|
|
125
|
+
shell: pwsh
|
|
126
|
+
run: ./packaging/windows/check-static-crt.ps1 -Path target/${{ matrix.target }}/debug/sigit.exe
|
|
@@ -96,6 +96,11 @@ jobs:
|
|
|
96
96
|
shell: bash
|
|
97
97
|
run: cargo build --locked --release --target ${{ matrix.target }}
|
|
98
98
|
|
|
99
|
+
- name: Check Windows binary has no VC++ runtime dependency
|
|
100
|
+
if: contains(matrix.target, 'windows')
|
|
101
|
+
shell: pwsh
|
|
102
|
+
run: ./packaging/windows/check-static-crt.ps1 -Path target/${{ matrix.target }}/release/${{ env.PROJECT_NAME }}.exe
|
|
103
|
+
|
|
99
104
|
- name: Package binary
|
|
100
105
|
shell: bash
|
|
101
106
|
run: |
|
|
@@ -351,6 +351,14 @@ GitHub release, so `release-github` builds the binaries, attaches the assets, an
|
|
|
351
351
|
is logged as a warning rather than failing the others, so an unconfigured channel does not block a
|
|
352
352
|
release. Their inputs live in `packaging/`.
|
|
353
353
|
|
|
354
|
+
The Windows binaries link the MSVC C runtime statically (`+crt-static` in `.cargo/config.toml`).
|
|
355
|
+
winget, Scoop, npm, PyPI and NuGet all ship the bare exe with no installer, so a dynamic build
|
|
356
|
+
that imports `VCRUNTIME140.dll` fails to load on a machine without the VC++ Redistributable
|
|
357
|
+
(`0xC0000135`, which is what failed winget's first validation).
|
|
358
|
+
`packaging/windows/check-static-crt.ps1` runs in CI and in `release-github` and fails the build if
|
|
359
|
+
a Windows exe imports the runtime again. Don't set a `RUSTFLAGS` env var in those workflows: it
|
|
360
|
+
replaces the config rustflags instead of adding to them.
|
|
361
|
+
|
|
354
362
|
Every release asset now carries a `.sha256` sidecar, not just the macOS Homebrew tarball. Scoop,
|
|
355
363
|
winget, and the AUR PKGBUILD each need one, and they consume the raw binaries rather than the
|
|
356
364
|
tarball. `release-github` also builds a `.deb` and `.rpm` per Linux target with nfpm
|
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 1.5.13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Three more siGit Code Cloud tiers: `flare`, `zenith` and `prism`.** They
|
|
10
|
+
show up in `/models` and in the editor's model picker alongside the existing
|
|
11
|
+
tiers. Selecting one needs a signed-in account, as before.
|
|
12
|
+
|
|
13
|
+
## 1.5.12
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **The Windows binaries start on machines without the Visual C++
|
|
18
|
+
Redistributable.** They used to import `VCRUNTIME140.dll` and the UCRT
|
|
19
|
+
`api-ms-win-crt-*` DLLs, so a clean Windows install refused to load them
|
|
20
|
+
with `STATUS_DLL_NOT_FOUND` (0xC0000135). That is what failed winget's
|
|
21
|
+
post-install check. The C runtime is now linked statically, and CI and the
|
|
22
|
+
release build fail if a Windows exe picks up that dependency again.
|
|
23
|
+
|
|
24
|
+
- **Watching a slow background command no longer ends the turn early.**
|
|
25
|
+
Polling `command_output` on the same task id three times tripped the
|
|
26
|
+
repeated-tool-call guard, which refused the call and dropped tools for the
|
|
27
|
+
rest of the turn. `command_output` now long-polls — it waits up to
|
|
28
|
+
`wait_seconds` (default 10, max 30) for the task to exit, then returns
|
|
29
|
+
everything printed in the meantime — and the repeat guard exempts it, since
|
|
30
|
+
polling the same task is the whole point of the tool.
|
|
31
|
+
|
|
32
|
+
- **A turn that ends with no visible text says why.** When the
|
|
33
|
+
repeated-tool-call guard blocked a call and the forced no-tools round came
|
|
34
|
+
back empty, sigit sent nothing more — the turn just stopped with no answer
|
|
35
|
+
and no question. When the last round of a tool-using turn produces no
|
|
36
|
+
visible text, sigit now sends a short closing message naming the repeated
|
|
37
|
+
tool and asking you to reply "continue" or redirect.
|
|
38
|
+
|
|
5
39
|
## 1.5.11
|
|
6
40
|
|
|
7
41
|
### Added
|
|
@@ -351,6 +351,14 @@ GitHub release, so `release-github` builds the binaries, attaches the assets, an
|
|
|
351
351
|
is logged as a warning rather than failing the others, so an unconfigured channel does not block a
|
|
352
352
|
release. Their inputs live in `packaging/`.
|
|
353
353
|
|
|
354
|
+
The Windows binaries link the MSVC C runtime statically (`+crt-static` in `.cargo/config.toml`).
|
|
355
|
+
winget, Scoop, npm, PyPI and NuGet all ship the bare exe with no installer, so a dynamic build
|
|
356
|
+
that imports `VCRUNTIME140.dll` fails to load on a machine without the VC++ Redistributable
|
|
357
|
+
(`0xC0000135`, which is what failed winget's first validation).
|
|
358
|
+
`packaging/windows/check-static-crt.ps1` runs in CI and in `release-github` and fails the build if
|
|
359
|
+
a Windows exe imports the runtime again. Don't set a `RUSTFLAGS` env var in those workflows: it
|
|
360
|
+
replaces the config rustflags instead of adding to them.
|
|
361
|
+
|
|
354
362
|
Every release asset now carries a `.sha256` sidecar, not just the macOS Homebrew tarball. Scoop,
|
|
355
363
|
winget, and the AUR PKGBUILD each need one, and they consume the raw binaries rather than the
|
|
356
364
|
tarball. `release-github` also builds a `.deb` and `.rpm` per Linux target with nfpm
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Fails if a Windows binary depends on the Visual C++ runtime DLLs.
|
|
2
|
+
#
|
|
3
|
+
# sigit is linked with +crt-static (see .cargo/config.toml) so it runs on a
|
|
4
|
+
# clean Windows install. If RUSTFLAGS overrides that, or a native dependency
|
|
5
|
+
# drags in the DLL runtime, the exe still builds and passes tests on a runner
|
|
6
|
+
# that has the redistributable installed, then fails to load on users'
|
|
7
|
+
# machines. This check catches it before release.
|
|
8
|
+
param(
|
|
9
|
+
[Parameter(Mandatory = $true)]
|
|
10
|
+
[string]$Path
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
$ErrorActionPreference = 'Stop'
|
|
14
|
+
|
|
15
|
+
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
|
16
|
+
$dumpbin = & $vswhere -latest -products * -find 'VC\Tools\MSVC\*\bin\Hostx64\x64\dumpbin.exe' |
|
|
17
|
+
Select-Object -First 1
|
|
18
|
+
if (-not $dumpbin) {
|
|
19
|
+
throw 'dumpbin.exe not found; is the MSVC toolset installed?'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
$output = & $dumpbin /nologo /dependents $Path
|
|
23
|
+
if ($LASTEXITCODE -ne 0) {
|
|
24
|
+
throw "dumpbin failed on $Path"
|
|
25
|
+
}
|
|
26
|
+
$output | Write-Host
|
|
27
|
+
|
|
28
|
+
$runtime = $output |
|
|
29
|
+
Where-Object { $_ -match '^\s+(vcruntime|msvcp|concrt|ucrtbase|api-ms-win-crt-)\S*\.dll\s*$' } |
|
|
30
|
+
ForEach-Object { $_.Trim() }
|
|
31
|
+
if ($runtime) {
|
|
32
|
+
Write-Host "::error::$Path depends on the Visual C++ runtime: $($runtime -join ', ')"
|
|
33
|
+
exit 1
|
|
34
|
+
}
|
|
35
|
+
Write-Host "$Path does not depend on the Visual C++ runtime."
|
{sigit_code-1.5.11 → sigit_code-1.5.13}/packaging/winget/getSigit.siGitCode.installer.yaml.in
RENAMED
|
@@ -6,6 +6,8 @@ InstallerType: portable
|
|
|
6
6
|
# assets are sigit-win-<arch>.exe, but the command has to be `sigit`.
|
|
7
7
|
Commands:
|
|
8
8
|
- sigit
|
|
9
|
+
# No VCRedist dependency: the exe links the C runtime statically
|
|
10
|
+
# (.cargo/config.toml), and CI rejects a build that imports it.
|
|
9
11
|
ReleaseDate: "@RELEASE_DATE@"
|
|
10
12
|
Installers:
|
|
11
13
|
- Architecture: x64
|
|
@@ -266,6 +266,21 @@ pub(crate) fn system_prompt_for_model(tool_calling: bool) -> String {
|
|
|
266
266
|
/// context window, so the cap can afford to be generous
|
|
267
267
|
const MAX_TOOL_ROUNDS: usize = 24;
|
|
268
268
|
|
|
269
|
+
/// What the user sees when a turn that ran tools ends with no closing text.
|
|
270
|
+
fn silent_stop_message(repeated_tool: Option<&str>, rounds: usize) -> String {
|
|
271
|
+
match repeated_tool {
|
|
272
|
+
Some(tool) => format!(
|
|
273
|
+
"I stopped because I kept making the same `{tool}` call without getting \
|
|
274
|
+
anywhere, and I had nothing to add without it. Reply \"continue\" to pick \
|
|
275
|
+
it back up, or tell me what to try instead."
|
|
276
|
+
),
|
|
277
|
+
None => format!(
|
|
278
|
+
"I stopped after {rounds} tool round(s) without writing a final answer. \
|
|
279
|
+
Reply \"continue\" to pick it back up, or tell me what to try instead."
|
|
280
|
+
),
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
269
284
|
/// Separator between two stretches of assistant prose that a tool round split
|
|
270
285
|
/// apart. ACP clients concatenate consecutive agent-message chunks into a
|
|
271
286
|
/// single block, so a plain newline (or nothing at all) leaves the rounds
|
|
@@ -2231,6 +2246,11 @@ impl SiGitAgent {
|
|
|
2231
2246
|
};
|
|
2232
2247
|
|
|
2233
2248
|
let mut round = 0;
|
|
2249
|
+
// Set when the repetition guard blocked a call; names the tool.
|
|
2250
|
+
let mut stopped_repeating: Option<String> = None;
|
|
2251
|
+
// Visible text already sent before the latest inference round, so the
|
|
2252
|
+
// end of the turn can tell whether that round said anything at all.
|
|
2253
|
+
let mut sent_before_last_round = 0;
|
|
2234
2254
|
|
|
2235
2255
|
while !result.tool_calls.is_empty() && round < MAX_TOOL_ROUNDS {
|
|
2236
2256
|
if cancellation.cancelled.load(Ordering::Acquire) {
|
|
@@ -2332,9 +2352,14 @@ impl SiGitAgent {
|
|
|
2332
2352
|
.entry(signature)
|
|
2333
2353
|
.and_modify(|count| *count += 1)
|
|
2334
2354
|
.or_insert(1);
|
|
2335
|
-
|
|
2355
|
+
// Repeated status checks are expected for a live background
|
|
2356
|
+
// command. command_output long-polls and the overall round cap
|
|
2357
|
+
// still bounds a confused model without cutting off a real
|
|
2358
|
+
// build or release while it is in progress.
|
|
2359
|
+
let repeated = *repeat_count >= 3 && tc.name != "command_output";
|
|
2336
2360
|
if repeated {
|
|
2337
2361
|
force_text = true;
|
|
2362
|
+
stopped_repeating = Some(tc.name.clone());
|
|
2338
2363
|
log::warn!(
|
|
2339
2364
|
"prompt({}) stopping repeated tool call `{}` after {} attempts",
|
|
2340
2365
|
session_id,
|
|
@@ -2468,6 +2493,7 @@ impl SiGitAgent {
|
|
|
2468
2493
|
// Whatever this round says starts a new paragraph rather than
|
|
2469
2494
|
// continuing the sentence the tool calls interrupted.
|
|
2470
2495
|
reply.interrupt();
|
|
2496
|
+
sent_before_last_round = reply.sent.len();
|
|
2471
2497
|
|
|
2472
2498
|
let cancelled_results = tool_results.clone();
|
|
2473
2499
|
result = match self
|
|
@@ -2510,7 +2536,7 @@ impl SiGitAgent {
|
|
|
2510
2536
|
session_id,
|
|
2511
2537
|
round
|
|
2512
2538
|
);
|
|
2513
|
-
|
|
2539
|
+
silent_stop_message(stopped_repeating.as_deref(), round)
|
|
2514
2540
|
} else {
|
|
2515
2541
|
log::warn!(
|
|
2516
2542
|
"prompt({}) — model returned empty reply (no tool rounds)",
|
|
@@ -2528,6 +2554,25 @@ impl SiGitAgent {
|
|
|
2528
2554
|
self.send_assistant_message(cx, session_id.clone(), final_text)
|
|
2529
2555
|
.ok();
|
|
2530
2556
|
}
|
|
2557
|
+
} else if round > 0 && reply.sent.len() == sent_before_last_round {
|
|
2558
|
+
// Earlier rounds streamed text, but the last one (typically the
|
|
2559
|
+
// forced no-tools round) said nothing. Without a closing message
|
|
2560
|
+
// the turn just stops, and the user can't tell whether it ended,
|
|
2561
|
+
// stalled, or is waiting on them.
|
|
2562
|
+
log::warn!(
|
|
2563
|
+
"prompt({}) — final round after {} tool round(s) produced no text",
|
|
2564
|
+
session_id,
|
|
2565
|
+
round
|
|
2566
|
+
);
|
|
2567
|
+
self.send_assistant_message(
|
|
2568
|
+
cx,
|
|
2569
|
+
session_id.clone(),
|
|
2570
|
+
format!(
|
|
2571
|
+
"{PARAGRAPH_BREAK}{}",
|
|
2572
|
+
silent_stop_message(stopped_repeating.as_deref(), round)
|
|
2573
|
+
),
|
|
2574
|
+
)
|
|
2575
|
+
.ok();
|
|
2531
2576
|
}
|
|
2532
2577
|
|
|
2533
2578
|
// Persist the completed turn so a restart (or session/load) can pick
|
|
@@ -40,7 +40,7 @@ const DEFAULT_CLOUD_URL: &str = "https://sigit.si/api/v1";
|
|
|
40
40
|
/// to be updated together when a tier is added.
|
|
41
41
|
pub const CLOUD_TIERS: &[&str] = &[
|
|
42
42
|
"fast", "balanced", "large", "mini", "air", "pro", "oke", "flux", "apex", "aura", "orbit",
|
|
43
|
-
"nova",
|
|
43
|
+
"nova", "flare", "zenith", "prism",
|
|
44
44
|
];
|
|
45
45
|
|
|
46
46
|
pub const DEFAULT_CLOUD_TIER: &str = "nova";
|
|
@@ -85,6 +85,9 @@ fn tier_to_model(tier: &str) -> String {
|
|
|
85
85
|
"aura" => "onde-aura",
|
|
86
86
|
"orbit" => "onde-orbit",
|
|
87
87
|
"nova" => "onde-nova",
|
|
88
|
+
"flare" => "onde-flare",
|
|
89
|
+
"zenith" => "onde-zenith",
|
|
90
|
+
"prism" => "onde-prism",
|
|
88
91
|
other => other,
|
|
89
92
|
}
|
|
90
93
|
.to_string()
|
|
@@ -267,4 +270,16 @@ mod tests {
|
|
|
267
270
|
assert_eq!(DEFAULT_CLOUD_TIER, "nova");
|
|
268
271
|
assert_eq!(tier_to_model(DEFAULT_CLOUD_TIER), "onde-nova");
|
|
269
272
|
}
|
|
273
|
+
|
|
274
|
+
#[test]
|
|
275
|
+
fn maps_every_cloud_tier_to_its_own_onde_model_id() {
|
|
276
|
+
for tier in CLOUD_TIERS {
|
|
277
|
+
// `oke` is the one tier whose wire id is not its own name.
|
|
278
|
+
let expected = match *tier {
|
|
279
|
+
"oke" => "onde-kkk".to_string(),
|
|
280
|
+
name => format!("onde-{name}"),
|
|
281
|
+
};
|
|
282
|
+
assert_eq!(tier_to_model(tier), expected);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
270
285
|
}
|
|
@@ -390,9 +390,10 @@ pub fn all_tools() -> Vec<AgentTool> {
|
|
|
390
390
|
name: "command_output",
|
|
391
391
|
description: "Get the output a background task (started with run_command's \
|
|
392
392
|
run_in_background) has produced since your last check, plus its \
|
|
393
|
-
status: still running, or exited with an exit code.
|
|
394
|
-
|
|
395
|
-
|
|
393
|
+
status: still running, or exited with an exit code. By default the \
|
|
394
|
+
call waits up to 10 seconds for new output or completion, so call it \
|
|
395
|
+
again when the task is still running rather than ending the turn. \
|
|
396
|
+
Between polls output is buffered up to 50 000 bytes per task; older \
|
|
396
397
|
output beyond that is dropped and the truncation is noted.",
|
|
397
398
|
parameters_schema: json!({
|
|
398
399
|
"type": "object",
|
|
@@ -400,6 +401,12 @@ pub fn all_tools() -> Vec<AgentTool> {
|
|
|
400
401
|
"task_id": {
|
|
401
402
|
"type": "integer",
|
|
402
403
|
"description": "Id of the background task, as returned by run_command with run_in_background."
|
|
404
|
+
},
|
|
405
|
+
"wait_seconds": {
|
|
406
|
+
"type": "integer",
|
|
407
|
+
"minimum": 0,
|
|
408
|
+
"maximum": 30,
|
|
409
|
+
"description": "Seconds to wait for new output or task completion before returning. Defaults to 10; use 0 for an immediate status check."
|
|
403
410
|
}
|
|
404
411
|
},
|
|
405
412
|
"required": ["task_id"],
|
|
@@ -458,6 +465,7 @@ async fn execute_tool_impl(name: &str, arguments: &str) -> String {
|
|
|
458
465
|
match name {
|
|
459
466
|
TASK_TOOL_NAME => exec_task(arguments).await,
|
|
460
467
|
WEB_SEARCH_TOOL_NAME => exec_web_search(arguments).await,
|
|
468
|
+
"command_output" => exec_command_output_wait(arguments, active_session()).await,
|
|
461
469
|
// Tools discovered from MCP servers are namespaced `mcp__<server>__<tool>`
|
|
462
470
|
// and forwarded to the owning server.
|
|
463
471
|
_ if crate::mcp::is_mcp_tool(name) => crate::mcp::call_tool(name, arguments).await,
|
|
@@ -515,7 +523,6 @@ fn execute_sync_tool(name: &str, arguments: &str, owner: Option<&str>) -> String
|
|
|
515
523
|
"remember" => exec_remember(arguments),
|
|
516
524
|
"delete_file" => exec_delete_file(arguments),
|
|
517
525
|
"run_command" => exec_run_command(arguments, owner),
|
|
518
|
-
"command_output" => exec_command_output(arguments, owner),
|
|
519
526
|
"kill_command" => exec_kill_command(arguments, owner),
|
|
520
527
|
"skill" => crate::skills::activate_skill(arguments),
|
|
521
528
|
_ => format!("Unknown tool: {name}"),
|
|
@@ -2506,9 +2513,38 @@ fn dropped_note(dropped: bool) -> &'static str {
|
|
|
2506
2513
|
|
|
2507
2514
|
/// `command_output` tool: output since the last poll + running/exited status.
|
|
2508
2515
|
fn exec_command_output(arguments: &str, owner: Option<&str>) -> String {
|
|
2516
|
+
poll_command_output(arguments, owner, false).text
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
/// One `command_output` check, with the tool text and whether the task is
|
|
2520
|
+
/// still running, so the long-poll loop knows when to keep waiting.
|
|
2521
|
+
struct CommandPoll {
|
|
2522
|
+
text: String,
|
|
2523
|
+
/// True while the task's child process is still running.
|
|
2524
|
+
running: bool,
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
impl CommandPoll {
|
|
2528
|
+
/// A result that ends the wait: an error, or a task that isn't ours.
|
|
2529
|
+
fn done(text: String) -> Self {
|
|
2530
|
+
Self {
|
|
2531
|
+
text,
|
|
2532
|
+
running: false,
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
/// `hold_while_running` is the long-poll's "not my last look" flag: a task
|
|
2538
|
+
/// that is still running keeps its output buffered and the returned text is
|
|
2539
|
+
/// empty, so the call that finally reports is the one that drains it.
|
|
2540
|
+
fn poll_command_output(
|
|
2541
|
+
arguments: &str,
|
|
2542
|
+
owner: Option<&str>,
|
|
2543
|
+
hold_while_running: bool,
|
|
2544
|
+
) -> CommandPoll {
|
|
2509
2545
|
let task_id = match parse_task_id(arguments) {
|
|
2510
2546
|
Ok(id) => id,
|
|
2511
|
-
Err(err) => return err,
|
|
2547
|
+
Err(err) => return CommandPoll::done(err),
|
|
2512
2548
|
};
|
|
2513
2549
|
|
|
2514
2550
|
let mut map = lock_tasks();
|
|
@@ -2516,16 +2552,23 @@ fn exec_command_output(arguments: &str, owner: Option<&str>) -> String {
|
|
|
2516
2552
|
.get_mut(&task_id)
|
|
2517
2553
|
.filter(|task| task.owner.as_deref() == owner)
|
|
2518
2554
|
else {
|
|
2519
|
-
return unknown_task(task_id);
|
|
2555
|
+
return CommandPoll::done(unknown_task(task_id));
|
|
2520
2556
|
};
|
|
2521
2557
|
|
|
2522
2558
|
let was_running = task.exit_code.is_none();
|
|
2523
2559
|
let exit_code = poll_exit_code(task);
|
|
2560
|
+
if hold_while_running && exit_code.is_none() {
|
|
2561
|
+
return CommandPoll {
|
|
2562
|
+
text: String::new(),
|
|
2563
|
+
running: true,
|
|
2564
|
+
};
|
|
2565
|
+
}
|
|
2524
2566
|
if was_running && exit_code.is_some() {
|
|
2525
2567
|
// The child just exited; give the reader threads a moment to flush
|
|
2526
2568
|
// the final output through the pipes before draining the buffer.
|
|
2527
2569
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
2528
2570
|
}
|
|
2571
|
+
|
|
2529
2572
|
let (new_output, dropped) = drain_task_output(task);
|
|
2530
2573
|
|
|
2531
2574
|
let command = &task.command;
|
|
@@ -2535,7 +2578,8 @@ fn exec_command_output(arguments: &str, owner: Option<&str>) -> String {
|
|
|
2535
2578
|
Some(code) => format!("Task {task_id} (`{command}`) exited with code {code}."),
|
|
2536
2579
|
};
|
|
2537
2580
|
|
|
2538
|
-
|
|
2581
|
+
let running = exit_code.is_none();
|
|
2582
|
+
let text = if new_output.is_empty() {
|
|
2539
2583
|
format!(
|
|
2540
2584
|
"{status} No new output since the last check.{}",
|
|
2541
2585
|
dropped_note(dropped)
|
|
@@ -2545,6 +2589,44 @@ fn exec_command_output(arguments: &str, owner: Option<&str>) -> String {
|
|
|
2545
2589
|
"{status} New output since the last check:{}\n{new_output}",
|
|
2546
2590
|
dropped_note(dropped)
|
|
2547
2591
|
)
|
|
2592
|
+
};
|
|
2593
|
+
CommandPoll { text, running }
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
/// Long-poll a background task without occupying Tokio's blocking pool while
|
|
2597
|
+
/// nothing changes. This keeps model tool rounds from being consumed by a
|
|
2598
|
+
/// tight status-check loop.
|
|
2599
|
+
async fn exec_command_output_wait(arguments: &str, owner: Option<String>) -> String {
|
|
2600
|
+
const DEFAULT_WAIT_SECONDS: u64 = 10;
|
|
2601
|
+
const MAX_WAIT_SECONDS: u64 = 30;
|
|
2602
|
+
const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(200);
|
|
2603
|
+
|
|
2604
|
+
let wait_seconds = serde_json::from_str::<Value>(arguments)
|
|
2605
|
+
.ok()
|
|
2606
|
+
.and_then(|args| args.get("wait_seconds").and_then(Value::as_u64))
|
|
2607
|
+
.unwrap_or(DEFAULT_WAIT_SECONDS)
|
|
2608
|
+
.min(MAX_WAIT_SECONDS);
|
|
2609
|
+
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(wait_seconds);
|
|
2610
|
+
|
|
2611
|
+
loop {
|
|
2612
|
+
let arguments = arguments.to_owned();
|
|
2613
|
+
let owner = owner.clone();
|
|
2614
|
+
// Every poll drains the task's buffer, so only the last look may take
|
|
2615
|
+
// the output of a running task; an earlier one would throw it away.
|
|
2616
|
+
let last_look = wait_seconds == 0 || tokio::time::Instant::now() >= deadline;
|
|
2617
|
+
let poll = tokio::task::spawn_blocking(move || {
|
|
2618
|
+
poll_command_output(&arguments, owner.as_deref(), !last_look)
|
|
2619
|
+
})
|
|
2620
|
+
.await
|
|
2621
|
+
.unwrap_or_else(|err| {
|
|
2622
|
+
CommandPoll::done(format!("Error: command_output task failed: {err}"))
|
|
2623
|
+
});
|
|
2624
|
+
|
|
2625
|
+
if last_look || !poll.running {
|
|
2626
|
+
return poll.text;
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
tokio::time::sleep(POLL_INTERVAL).await;
|
|
2548
2630
|
}
|
|
2549
2631
|
}
|
|
2550
2632
|
|
|
@@ -3726,6 +3808,14 @@ mod tests {
|
|
|
3726
3808
|
}
|
|
3727
3809
|
}
|
|
3728
3810
|
|
|
3811
|
+
/// The output part of a `command_output` reply. The status line quotes
|
|
3812
|
+
/// the command, so matching the whole reply finds "echo done" whether or
|
|
3813
|
+
/// not `done` was ever printed.
|
|
3814
|
+
fn polled_output(poll: &str) -> &str {
|
|
3815
|
+
poll.split_once("New output since the last check:")
|
|
3816
|
+
.map_or("", |(_, output)| output)
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3729
3819
|
/// Wait (bounded) for a background task's child to exit.
|
|
3730
3820
|
fn wait_for_background_exit(task_id: u64) {
|
|
3731
3821
|
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
|
@@ -3783,6 +3873,109 @@ mod tests {
|
|
|
3783
3873
|
assert!(combined.contains("done"), "got: {combined}");
|
|
3784
3874
|
}
|
|
3785
3875
|
|
|
3876
|
+
#[tokio::test]
|
|
3877
|
+
async fn test_command_output_waits_for_background_completion() {
|
|
3878
|
+
#[cfg(unix)]
|
|
3879
|
+
let command = "sleep 1; echo done";
|
|
3880
|
+
#[cfg(windows)]
|
|
3881
|
+
let command = "ping -n 2 127.0.0.1 > nul&& echo done";
|
|
3882
|
+
|
|
3883
|
+
let args = serde_json::json!({
|
|
3884
|
+
"command": command,
|
|
3885
|
+
"cwd": std::env::temp_dir(),
|
|
3886
|
+
"run_in_background": true
|
|
3887
|
+
})
|
|
3888
|
+
.to_string();
|
|
3889
|
+
let result = exec_run_command(&args, None);
|
|
3890
|
+
let task_id = background_task_id(&result);
|
|
3891
|
+
|
|
3892
|
+
let poll_args = serde_json::json!({
|
|
3893
|
+
"task_id": task_id,
|
|
3894
|
+
"wait_seconds": 5
|
|
3895
|
+
})
|
|
3896
|
+
.to_string();
|
|
3897
|
+
let poll = exec_command_output_wait(&poll_args, None).await;
|
|
3898
|
+
|
|
3899
|
+
assert!(poll.contains("exited with code 0"), "got: {poll}");
|
|
3900
|
+
assert!(polled_output(&poll).contains("done"), "got: {poll}");
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
#[tokio::test]
|
|
3904
|
+
async fn test_command_output_wait_keeps_output_printed_before_exit() {
|
|
3905
|
+
#[cfg(unix)]
|
|
3906
|
+
let command = "echo start; sleep 1; echo done";
|
|
3907
|
+
#[cfg(windows)]
|
|
3908
|
+
let command = "echo start&& ping -n 2 127.0.0.1 > nul&& echo done";
|
|
3909
|
+
|
|
3910
|
+
let args = serde_json::json!({
|
|
3911
|
+
"command": command,
|
|
3912
|
+
"cwd": std::env::temp_dir(),
|
|
3913
|
+
"run_in_background": true
|
|
3914
|
+
})
|
|
3915
|
+
.to_string();
|
|
3916
|
+
let task_id = background_task_id(&exec_run_command(&args, None));
|
|
3917
|
+
|
|
3918
|
+
let poll_args = serde_json::json!({ "task_id": task_id, "wait_seconds": 5 }).to_string();
|
|
3919
|
+
let poll = exec_command_output_wait(&poll_args, None).await;
|
|
3920
|
+
|
|
3921
|
+
// "start" is printed well before the exit the wait returns on; the
|
|
3922
|
+
// polls in between must not have drained it away.
|
|
3923
|
+
assert!(poll.contains("exited with code 0"), "got: {poll}");
|
|
3924
|
+
assert!(polled_output(&poll).contains("start"), "got: {poll}");
|
|
3925
|
+
assert!(polled_output(&poll).contains("done"), "got: {poll}");
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
#[tokio::test]
|
|
3929
|
+
async fn test_command_output_wait_reports_output_of_a_running_task() {
|
|
3930
|
+
#[cfg(unix)]
|
|
3931
|
+
let command = "echo start; sleep 30";
|
|
3932
|
+
#[cfg(windows)]
|
|
3933
|
+
let command = "echo start&& ping -n 31 127.0.0.1 > nul";
|
|
3934
|
+
|
|
3935
|
+
let args = serde_json::json!({
|
|
3936
|
+
"command": command,
|
|
3937
|
+
"cwd": std::env::temp_dir(),
|
|
3938
|
+
"run_in_background": true
|
|
3939
|
+
})
|
|
3940
|
+
.to_string();
|
|
3941
|
+
let task_id = background_task_id(&exec_run_command(&args, None));
|
|
3942
|
+
|
|
3943
|
+
let poll_args = serde_json::json!({ "task_id": task_id, "wait_seconds": 1 }).to_string();
|
|
3944
|
+
let poll = exec_command_output_wait(&poll_args, None).await;
|
|
3945
|
+
|
|
3946
|
+
assert!(poll.contains("still running"), "got: {poll}");
|
|
3947
|
+
assert!(polled_output(&poll).contains("start"), "got: {poll}");
|
|
3948
|
+
exec_kill_command(&serde_json::json!({ "task_id": task_id }).to_string(), None);
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
#[tokio::test]
|
|
3952
|
+
async fn test_command_output_waits_out_a_quiet_running_task() {
|
|
3953
|
+
#[cfg(unix)]
|
|
3954
|
+
let command = "sleep 30";
|
|
3955
|
+
#[cfg(windows)]
|
|
3956
|
+
let command = "ping -n 31 127.0.0.1 > nul";
|
|
3957
|
+
|
|
3958
|
+
let args = serde_json::json!({
|
|
3959
|
+
"command": command,
|
|
3960
|
+
"cwd": std::env::temp_dir(),
|
|
3961
|
+
"run_in_background": true
|
|
3962
|
+
})
|
|
3963
|
+
.to_string();
|
|
3964
|
+
let task_id = background_task_id(&exec_run_command(&args, None));
|
|
3965
|
+
|
|
3966
|
+
let poll_args = serde_json::json!({ "task_id": task_id, "wait_seconds": 1 }).to_string();
|
|
3967
|
+
let started = std::time::Instant::now();
|
|
3968
|
+
let poll = exec_command_output_wait(&poll_args, None).await;
|
|
3969
|
+
|
|
3970
|
+
assert!(poll.contains("still running"), "got: {poll}");
|
|
3971
|
+
assert!(
|
|
3972
|
+
started.elapsed() >= std::time::Duration::from_millis(900),
|
|
3973
|
+
"an idle task should hold the poll for the whole window, returned after {:?}",
|
|
3974
|
+
started.elapsed()
|
|
3975
|
+
);
|
|
3976
|
+
exec_kill_command(&serde_json::json!({ "task_id": task_id }).to_string(), None);
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3786
3979
|
#[test]
|
|
3787
3980
|
fn test_kill_command_stops_background_task() {
|
|
3788
3981
|
#[cfg(unix)]
|
|
@@ -20,7 +20,9 @@ use std::time::{Duration, Instant};
|
|
|
20
20
|
|
|
21
21
|
use serde_json::{Value, json};
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
/// This is the first test file to launch the freshly built binary, and on the
|
|
24
|
+
/// Intel macOS runner that first launch alone has taken 25 seconds.
|
|
25
|
+
const TIMEOUT: Duration = Duration::from_secs(60);
|
|
24
26
|
|
|
25
27
|
const ALLOWANCE_MESSAGE: &str = "Monthly siGit Code Cloud allowance reached. It resets at the start of your next billing period.";
|
|
26
28
|
|
|
@@ -1038,11 +1038,11 @@ fn an_unparseable_tool_call_is_hidden_and_retried() {
|
|
|
1038
1038
|
/// response without being executed or recorded as orphaned history entries.
|
|
1039
1039
|
#[test]
|
|
1040
1040
|
fn forced_text_suppresses_glm_check_status_tool_markup() {
|
|
1041
|
-
let repeated_arguments = json!({"
|
|
1041
|
+
let repeated_arguments = json!({"path": "missing.txt"}).to_string();
|
|
1042
1042
|
let endpoint = start_fake_endpoint(vec![
|
|
1043
|
-
sse_tool_call("call_1", "
|
|
1044
|
-
sse_tool_call("call_2", "
|
|
1045
|
-
sse_tool_call("call_3", "
|
|
1043
|
+
sse_tool_call("call_1", "read_file", &repeated_arguments),
|
|
1044
|
+
sse_tool_call("call_2", "read_file", &repeated_arguments),
|
|
1045
|
+
sse_tool_call("call_3", "read_file", &repeated_arguments),
|
|
1046
1046
|
sse_body(&[
|
|
1047
1047
|
json!({"choices": [{"delta": {"content": "Build is still running. <tool_call>command_output CheckStatus=true_or_poll_"}}]}),
|
|
1048
1048
|
json!({"choices": [{"delta": {"content": "again_with_different_params</arg_value><arg_key>task_id</arg_key><arg_value>1</arg_value></tool_call>"}}]}),
|
|
@@ -1227,6 +1227,113 @@ fn a_kimi_k3_tool_call_emitted_as_text_is_executed_rather_than_rendered() {
|
|
|
1227
1227
|
let _ = std::fs::remove_dir_all(&scratch);
|
|
1228
1228
|
}
|
|
1229
1229
|
|
|
1230
|
+
#[test]
|
|
1231
|
+
fn a_silent_forced_text_round_still_ends_with_a_message() {
|
|
1232
|
+
let repeated_arguments = json!({"path": "missing.txt"}).to_string();
|
|
1233
|
+
let endpoint = start_fake_endpoint(vec![
|
|
1234
|
+
sse_text_then_tool_call(
|
|
1235
|
+
"Checking the file.",
|
|
1236
|
+
"call_1",
|
|
1237
|
+
"read_file",
|
|
1238
|
+
&repeated_arguments,
|
|
1239
|
+
),
|
|
1240
|
+
sse_tool_call("call_2", "read_file", &repeated_arguments),
|
|
1241
|
+
sse_tool_call("call_3", "read_file", &repeated_arguments),
|
|
1242
|
+
// The forced no-tools round says nothing, as in the stalled thread.
|
|
1243
|
+
sse_body(&[]),
|
|
1244
|
+
]);
|
|
1245
|
+
|
|
1246
|
+
let scratch =
|
|
1247
|
+
std::env::temp_dir().join(format!("sigit_acp_silent_stop_{}", std::process::id()));
|
|
1248
|
+
let config_dir = scratch.join("config");
|
|
1249
|
+
let cwd = scratch.join("cwd");
|
|
1250
|
+
std::fs::create_dir_all(&config_dir).unwrap();
|
|
1251
|
+
std::fs::create_dir_all(&cwd).unwrap();
|
|
1252
|
+
|
|
1253
|
+
let mut agent = spawn_agent(endpoint.port, &config_dir);
|
|
1254
|
+
let id = agent.request(
|
|
1255
|
+
"initialize",
|
|
1256
|
+
json!({"protocolVersion": 1, "clientCapabilities": {}}),
|
|
1257
|
+
);
|
|
1258
|
+
agent.wait_for_response(id);
|
|
1259
|
+
let id = agent.request("session/new", json!({"cwd": cwd, "mcpServers": []}));
|
|
1260
|
+
let session_id = agent.wait_for_response(id)["result"]["sessionId"]
|
|
1261
|
+
.as_str()
|
|
1262
|
+
.expect("session id")
|
|
1263
|
+
.to_string();
|
|
1264
|
+
|
|
1265
|
+
let prompt_id = agent.request(
|
|
1266
|
+
"session/prompt",
|
|
1267
|
+
json!({
|
|
1268
|
+
"sessionId": session_id,
|
|
1269
|
+
"prompt": [{"type": "text", "text": "read it"}],
|
|
1270
|
+
}),
|
|
1271
|
+
);
|
|
1272
|
+
let (response, rendered) = agent.wait_for_prompt(prompt_id);
|
|
1273
|
+
assert_eq!(response["result"]["stopReason"], "end_turn");
|
|
1274
|
+
assert!(rendered.contains("Checking the file."), "{rendered:?}");
|
|
1275
|
+
assert!(
|
|
1276
|
+
rendered.contains("same `read_file` call") && rendered.contains("continue"),
|
|
1277
|
+
"the turn ended without telling the user why: {rendered:?}"
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1280
|
+
drop(agent);
|
|
1281
|
+
let _ = std::fs::remove_dir_all(&scratch);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
#[test]
|
|
1285
|
+
fn repeated_command_output_calls_remain_available() {
|
|
1286
|
+
let repeated_arguments = json!({"task_id": 999}).to_string();
|
|
1287
|
+
let endpoint = start_fake_endpoint(vec![
|
|
1288
|
+
sse_tool_call("call_1", "command_output", &repeated_arguments),
|
|
1289
|
+
sse_tool_call("call_2", "command_output", &repeated_arguments),
|
|
1290
|
+
sse_tool_call("call_3", "command_output", &repeated_arguments),
|
|
1291
|
+
sse_text("The background task finished."),
|
|
1292
|
+
]);
|
|
1293
|
+
|
|
1294
|
+
let scratch =
|
|
1295
|
+
std::env::temp_dir().join(format!("sigit_acp_repeated_poll_{}", std::process::id()));
|
|
1296
|
+
let config_dir = scratch.join("config");
|
|
1297
|
+
let cwd = scratch.join("cwd");
|
|
1298
|
+
std::fs::create_dir_all(&config_dir).unwrap();
|
|
1299
|
+
std::fs::create_dir_all(&cwd).unwrap();
|
|
1300
|
+
|
|
1301
|
+
let mut agent = spawn_agent(endpoint.port, &config_dir);
|
|
1302
|
+
let id = agent.request(
|
|
1303
|
+
"initialize",
|
|
1304
|
+
json!({"protocolVersion": 1, "clientCapabilities": {}}),
|
|
1305
|
+
);
|
|
1306
|
+
agent.wait_for_response(id);
|
|
1307
|
+
let id = agent.request("session/new", json!({"cwd": cwd, "mcpServers": []}));
|
|
1308
|
+
let session_id = agent.wait_for_response(id)["result"]["sessionId"]
|
|
1309
|
+
.as_str()
|
|
1310
|
+
.expect("session id")
|
|
1311
|
+
.to_string();
|
|
1312
|
+
|
|
1313
|
+
let prompt_id = agent.request(
|
|
1314
|
+
"session/prompt",
|
|
1315
|
+
json!({
|
|
1316
|
+
"sessionId": session_id,
|
|
1317
|
+
"prompt": [{"type": "text", "text": "wait for the background task"}],
|
|
1318
|
+
}),
|
|
1319
|
+
);
|
|
1320
|
+
let (response, rendered) = agent.wait_for_prompt(prompt_id);
|
|
1321
|
+
assert_eq!(response["result"]["stopReason"], "end_turn");
|
|
1322
|
+
assert!(rendered.contains("finished"), "got: {rendered:?}");
|
|
1323
|
+
|
|
1324
|
+
let requests = endpoint.requests.lock().unwrap();
|
|
1325
|
+
assert_eq!(requests.len(), 4);
|
|
1326
|
+
assert!(
|
|
1327
|
+
requests[3].get("tools").is_some(),
|
|
1328
|
+
"command_output repetition must not force tools off: {:?}",
|
|
1329
|
+
requests[3]
|
|
1330
|
+
);
|
|
1331
|
+
drop(requests);
|
|
1332
|
+
|
|
1333
|
+
drop(agent);
|
|
1334
|
+
let _ = std::fs::remove_dir_all(&scratch);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1230
1337
|
#[test]
|
|
1231
1338
|
fn write_todos_reaches_the_client_as_an_acp_plan() {
|
|
1232
1339
|
let endpoint = start_fake_endpoint(vec![
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sigit_code-1.5.11 → sigit_code-1.5.13}/packaging/winget/getSigit.siGitCode.locale.en-US.yaml.in
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|