sigit-code 1.5.8__tar.gz → 1.5.10__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.8 → sigit_code-1.5.10}/.agents/skills/sigit-code-release/SKILL.md +1 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.claude/skills/sigit-code-release/SKILL.md +1 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.github/workflows/release-github.yml +36 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/CHANGELOG.md +64 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/Cargo.lock +1 -1
- {sigit_code-1.5.8 → sigit_code-1.5.10}/Cargo.toml +1 -1
- {sigit_code-1.5.8 → sigit_code-1.5.10}/PKG-INFO +1 -1
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/backend.rs +74 -29
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/chat.rs +3 -7
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/headless.rs +3 -7
- sigit_code-1.5.10/src/inline_tool_calls.rs +976 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/main.rs +53 -10
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/provider.rs +8 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/tools.rs +2 -6
- {sigit_code-1.5.8 → sigit_code-1.5.10}/tests/acp_permissions.rs +194 -0
- sigit_code-1.5.8/src/inline_tool_calls.rs +0 -420
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.agents/AGENTS.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.agents/skills/agent-client-protocol/SKILL.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.agents/skills/ai-assisted-coding/SKILL.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.agents/skills/branding/SKILL.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.agents/skills/run-sigit/SKILL.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.agents/skills/run-sigit/driver.mjs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.agents/skills/run-sigit/tui-smoke.sh +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.agents/skills/tool-calling/SKILL.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.claude/skills/agent-client-protocol/SKILL.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.claude/skills/ai-assisted-coding/SKILL.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.claude/skills/branding/SKILL.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.claude/skills/run-sigit/SKILL.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.claude/skills/run-sigit/driver.mjs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.claude/skills/run-sigit/tui-smoke.sh +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.claude/skills/tool-calling/SKILL.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.github/workflows/ci.yml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.github/workflows/release-aur.yml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.github/workflows/release-crates.yml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.github/workflows/release-homebrew.yml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.github/workflows/release-npm.yml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.github/workflows/release-nuget.yml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.github/workflows/release-pypi.yml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.github/workflows/release-scoop.yml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.github/workflows/release-winget.yml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.gitignore +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/.nvmrc +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/AGENTS.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/CLAUDE.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/LICENSE +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/README.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/docs/hooks.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/docs/mcp.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/examples/settings-with-hooks.toml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/examples/skills/README.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/examples/skills/commit-message/SKILL.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/npm/README.md.tmpl +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/npm/package-compat.json.tmpl +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/npm/package-main.json.tmpl +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/npm/package.json.tmpl +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/npm/scripts/render-main-package.cjs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/npm/scripts/render-platform-package.cjs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/npm/sigit/.gitignore +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/npm/sigit/README.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/npm/sigit/package.json +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/npm/sigit/src/index.ts +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/npm/sigit/tsconfig.json +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/nuget/.gitignore +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/nuget/sigit/Program.cs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/nuget/sigit/README.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/nuget/sigit/SiGit.Code.csproj +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/packaging/aur/PKGBUILD.in +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/packaging/nfpm.yaml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/packaging/winget/getSigit.siGitCode.installer.yaml.in +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/packaging/winget/getSigit.siGitCode.locale.en-US.yaml.in +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/packaging/winget/getSigit.siGitCode.yaml.in +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/pypi/README.md +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/pypi/pyproject.toml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/pyproject.toml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/rust-toolchain.toml +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/account.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/browser_auth.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/commands.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/credentials.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/frontmatter.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/hooks.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/instructions.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/mcp.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/models.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/permissions.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/session_store.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/settings.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/setup.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/skills.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/subagents.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/src/workspace.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/tests/acp_endpoint_errors.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/tests/acp_multi_root.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/tests/acp_session_load.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/tests/acp_tool_stdin.rs +0 -0
- {sigit_code-1.5.8 → sigit_code-1.5.10}/tests/headless_mode.rs +0 -0
|
@@ -35,6 +35,7 @@ Use this skill when preparing a release for this repository.
|
|
|
35
35
|
- The crate is published to crates.io (`release-crates.yml`) and the Homebrew tap is updated (`release-homebrew.yml`) as part of the tag-driven flow. Per the siGit release flow, Homebrew is auto-triggered — do not dispatch it manually.
|
|
36
36
|
- `release-github.yml` builds the binaries, attaches them (plus a `.deb`/`.rpm` per Linux target built with nfpm from `packaging/nfpm.yaml`) to the GitHub release, then dispatches `release-homebrew`, `release-scoop`, `release-winget`, and `release-aur`. A dispatch failure in any one of those four is logged as a warning, not a hard failure — an unconfigured channel doesn't block the rest of the release. `release-nuget.yml` and `release-mcp-registry.yml`, by contrast, are tag-triggered directly like the other language-registry workflows, not dispatched from `release-github.yml`.
|
|
37
37
|
- Every release asset carries a `.sha256` sidecar (not just the macOS Homebrew tarball). Scoop, winget, and the AUR PKGBUILD each consume the raw binaries plus their sidecar checksum, rather than the tarball.
|
|
38
|
+
- The GitHub release body is generated, not hand-written: `release-github.yml`'s release job checks out the tag, awk-extracts the `## <version>` section from `CHANGELOG.md` (a missing section is a hard failure), appends a full-changelog link and a `sigit.si/code` footnote, and publishes it via `body_path: release_notes.md`. The only file a release-prep change needs to touch is `CHANGELOG.md` — never edit release notes in the GitHub UI, because the next tag push would overwrite them.
|
|
38
39
|
- npm and NuGet publish via OIDC trusted publishing — neither has a stored token in the repo. On npm this is configured per package (each `@getsigit/*` name has a trusted publisher pointing at `release-npm.yml`), so a new package needs the same setup before its first publish will work. OIDC requires npm >= 11.5.1 and Node >= 22.14.0.
|
|
39
40
|
- The NuGet package (`SiGit.Code`, `dotnet tool install --global SiGit.Code`) bundles all six platform binaries under `native/<os>-<arch>/` behind a managed shim, rather than shipping one package per platform like npm/PyPI. Watch the nuget.org 250 MB package-size limit if it ever grows.
|
|
40
41
|
- The baked-in official MCP server is separately listed in the public MCP Registry as `si.sigit/sigit` (`release-mcp-registry.yml`, driven by `server.json` at the repo root). It's a remote Streamable-HTTP listing, so it's verified by a DNS TXT record on the `sigit.si` domain rather than the GitHub-OIDC scheme used for package listings.
|
|
@@ -35,6 +35,7 @@ Use this skill when preparing a release for this repository.
|
|
|
35
35
|
- The crate is published to crates.io (`release-crates.yml`) and the Homebrew tap is updated (`release-homebrew.yml`) as part of the tag-driven flow. Per the siGit release flow, Homebrew is auto-triggered — do not dispatch it manually.
|
|
36
36
|
- `release-github.yml` builds the binaries, attaches them (plus a `.deb`/`.rpm` per Linux target built with nfpm from `packaging/nfpm.yaml`) to the GitHub release, then dispatches `release-homebrew`, `release-scoop`, `release-winget`, and `release-aur`. A dispatch failure in any one of those four is logged as a warning, not a hard failure — an unconfigured channel doesn't block the rest of the release. `release-nuget.yml` and `release-mcp-registry.yml`, by contrast, are tag-triggered directly like the other language-registry workflows, not dispatched from `release-github.yml`.
|
|
37
37
|
- Every release asset carries a `.sha256` sidecar (not just the macOS Homebrew tarball). Scoop, winget, and the AUR PKGBUILD each consume the raw binaries plus their sidecar checksum, rather than the tarball.
|
|
38
|
+
- The GitHub release body is generated, not hand-written: `release-github.yml`'s release job checks out the tag, awk-extracts the `## <version>` section from `CHANGELOG.md` (a missing section is a hard failure), appends a full-changelog link and a `sigit.si/code` footnote, and publishes it via `body_path: release_notes.md`. The only file a release-prep change needs to touch is `CHANGELOG.md` — never edit release notes in the GitHub UI, because the next tag push would overwrite them.
|
|
38
39
|
- npm and NuGet publish via OIDC trusted publishing — neither has a stored token in the repo. On npm this is configured per package (each `@getsigit/*` name has a trusted publisher pointing at `release-npm.yml`), so a new package needs the same setup before its first publish will work. OIDC requires npm >= 11.5.1 and Node >= 22.14.0.
|
|
39
40
|
- The NuGet package (`SiGit.Code`, `dotnet tool install --global SiGit.Code`) bundles all six platform binaries under `native/<os>-<arch>/` behind a managed shim, rather than shipping one package per platform like npm/PyPI. Watch the nuget.org 250 MB package-size limit if it ever grows.
|
|
40
41
|
- The baked-in official MCP server is separately listed in the public MCP Registry as `si.sigit/sigit` (`release-mcp-registry.yml`, driven by `server.json` at the repo root). It's a remote Streamable-HTTP listing, so it's verified by a DNS TXT record on the `sigit.si` domain rather than the GitHub-OIDC scheme used for package listings.
|
|
@@ -240,6 +240,41 @@ jobs:
|
|
|
240
240
|
echo "tag=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
|
|
241
241
|
fi
|
|
242
242
|
|
|
243
|
+
- name: Checkout tag
|
|
244
|
+
# The release job otherwise only downloads artifacts; the changelog
|
|
245
|
+
# has to be read from the tree at the tag being released.
|
|
246
|
+
uses: actions/checkout@v6
|
|
247
|
+
with:
|
|
248
|
+
ref: ${{ steps.tag.outputs.tag }}
|
|
249
|
+
|
|
250
|
+
- name: Compose release notes
|
|
251
|
+
# The release page carries the section of CHANGELOG.md for this
|
|
252
|
+
# version, not a bare "Full changelog" default. A missing section is
|
|
253
|
+
# a hard failure: publishing a release whose notes don't match its
|
|
254
|
+
# changelog is worse than stopping the job.
|
|
255
|
+
shell: bash
|
|
256
|
+
run: |
|
|
257
|
+
version="${{ steps.tag.outputs.tag }}"
|
|
258
|
+
version="${version#v}"
|
|
259
|
+
section="$(awk -v ver="${version}" '
|
|
260
|
+
$0 == "## " ver { found = 1; next }
|
|
261
|
+
found && /^## / { exit }
|
|
262
|
+
found { print }
|
|
263
|
+
' CHANGELOG.md)"
|
|
264
|
+
if [ -z "${section//[$' \t\n']/}" ]; then
|
|
265
|
+
echo "No ## ${version} section found in CHANGELOG.md" >&2
|
|
266
|
+
exit 1
|
|
267
|
+
fi
|
|
268
|
+
{
|
|
269
|
+
printf '%s\n' "${section}"
|
|
270
|
+
printf '\n---\n\n'
|
|
271
|
+
printf 'Full changelog: https://github.com/%s/blob/%s/CHANGELOG.md\n' \
|
|
272
|
+
"${GITHUB_REPOSITORY}" "${{ steps.tag.outputs.tag }}"
|
|
273
|
+
printf '\n---\n\n'
|
|
274
|
+
printf 'About siGit Code: [sigit.si/code](https://sigit.si/code)\n'
|
|
275
|
+
} > release_notes.md
|
|
276
|
+
cat release_notes.md
|
|
277
|
+
|
|
243
278
|
- name: Download binary artifacts
|
|
244
279
|
uses: actions/download-artifact@v4
|
|
245
280
|
with:
|
|
@@ -258,6 +293,7 @@ jobs:
|
|
|
258
293
|
uses: softprops/action-gh-release@v2
|
|
259
294
|
with:
|
|
260
295
|
tag_name: ${{ steps.tag.outputs.tag }}
|
|
296
|
+
body_path: release_notes.md
|
|
261
297
|
files: release/*
|
|
262
298
|
|
|
263
299
|
# These channels all publish somewhere outside this repo (a tap, a Scoop
|
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.10
|
|
4
|
+
|
|
5
|
+
### What changed
|
|
6
|
+
|
|
7
|
+
- **Xcode's context no longer buries slash commands.** Xcode sends the user's
|
|
8
|
+
prompt as the last of several text blocks, with project context in the
|
|
9
|
+
blocks before it, so `/models` and friends sat in the middle of joined text
|
|
10
|
+
and were handed to the model instead of dispatching locally. Slash-command
|
|
11
|
+
parsing now searches the prompt's text blocks from the end, so a standalone
|
|
12
|
+
command in the final user block dispatches no matter what context the client
|
|
13
|
+
prepends
|
|
14
|
+
- **The nova tier is the default cloud engine.** When local inference is off
|
|
15
|
+
and no explicit provider override is set, the cloud tier now defaults to
|
|
16
|
+
`nova` (the `onde-nova` model) instead of the balanced tier, in both the
|
|
17
|
+
interactive and headless paths
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Malformed Chinese-model tool calls are handled end-to-end in ACP.** The
|
|
22
|
+
inline-call recovery that 1.5.9 added for Kimi K3's XTML protocol and GLM's
|
|
23
|
+
mis-tagged blocks only applied to the interactive and headless surfaces;
|
|
24
|
+
the ACP path never ran it, so an editor session still surfaced raw protocol
|
|
25
|
+
text and dropped the calls. Recovery now runs in the ACP prompt loop too,
|
|
26
|
+
covered by integration tests, and the tool names are checked against the
|
|
27
|
+
turn's offered tools before anything executes
|
|
28
|
+
- **Suppressed tool-call log spam is gone.** The guard that logs when a
|
|
29
|
+
structured call is dropped for arriving as forced text fired once per
|
|
30
|
+
streamed argument fragment — one malformed call could log a warning per
|
|
31
|
+
chunk. The check now fires once per turn with the count and names of what
|
|
32
|
+
was dropped, and the non-streaming path reports the same
|
|
33
|
+
- **A history-replay test no longer races on the process cwd.** One ACP test
|
|
34
|
+
built its expected path from `std::env::current_dir()` while other tests
|
|
35
|
+
briefly swap that process-global cwd, so on CI it could read a temp
|
|
36
|
+
directory mid-swap and compare a truncated title against an untruncated
|
|
37
|
+
expectation. It now uses a fixed path it never needed to derive
|
|
38
|
+
|
|
39
|
+
## 1.5.9
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- **Kimi K3 tool calls no longer arrive as raw protocol text.** Kimi K3 renders
|
|
44
|
+
a call in Moonshot's XTML protocol — pipe-delimited open/sep/close tokens
|
|
45
|
+
wrapping named blocks — and when the serving stack doesn't parse that back
|
|
46
|
+
into structured tool calls, the whole block surfaced in the editor as literal
|
|
47
|
+
text and the turn ended as though the model had answered in prose. The
|
|
48
|
+
inline-call recovery now speaks that protocol alongside the legacy XML tags:
|
|
49
|
+
tools blocks are parsed into real calls (several per block, argument values
|
|
50
|
+
decoded and typed from the block's own type attribute or, when it is
|
|
51
|
+
missing, the turn's tool schemas), response blocks are unwrapped so their
|
|
52
|
+
text still renders, and think blocks are dropped as private reasoning rather
|
|
53
|
+
than shown. The streaming scanner watches all the opening markers, so a
|
|
54
|
+
block split across chunk boundaries still recovers
|
|
55
|
+
- **GLM's mis-tagged calls recover too.** GLM sometimes precedes the legacy
|
|
56
|
+
block's first argument key with another tool-call opening tag instead of the
|
|
57
|
+
arg-key opening tag the format calls for, and the recovery rejected the
|
|
58
|
+
whole block when it did — the call surfaced as text and whatever it was
|
|
59
|
+
trying to do was dropped. The parser now accepts that alias for the first
|
|
60
|
+
argument only — later arguments stay strict, so text that merely resembles a
|
|
61
|
+
call cannot be recovered as one — in both the whole-blob and streaming
|
|
62
|
+
paths, and the scanner tolerates the malformed marker split across chunk
|
|
63
|
+
boundaries. Recovery in both protocols also checks the call's name against
|
|
64
|
+
the tools actually offered in the turn: a tag naming a tool that was never
|
|
65
|
+
in the turn's spec stays on screen instead of being executed
|
|
66
|
+
|
|
3
67
|
## 1.5.8
|
|
4
68
|
|
|
5
69
|
### What changed
|
|
@@ -159,12 +159,15 @@ pub trait InferenceBackend: Send + Sync {
|
|
|
159
159
|
sink: Option<&TokenSink>,
|
|
160
160
|
) -> Result<TurnResult, BackendError>;
|
|
161
161
|
|
|
162
|
-
/// Continue the turn by returning tool results. `
|
|
163
|
-
///
|
|
162
|
+
/// Continue the turn by returning tool results. `allow_tool_calls` controls
|
|
163
|
+
/// whether `tools` are offered again; the complete catalog remains available
|
|
164
|
+
/// so a disabled round can recognize and suppress tool-shaped model output.
|
|
165
|
+
/// `sink` streams assistant text when set.
|
|
164
166
|
async fn send_tool_results(
|
|
165
167
|
&self,
|
|
166
168
|
results: Vec<ToolResult>,
|
|
167
|
-
tools:
|
|
169
|
+
tools: &[ToolSpec],
|
|
170
|
+
allow_tool_calls: bool,
|
|
168
171
|
sink: Option<&TokenSink>,
|
|
169
172
|
) -> Result<TurnResult, BackendError>;
|
|
170
173
|
|
|
@@ -257,7 +260,8 @@ impl InferenceBackend for LocalBackend {
|
|
|
257
260
|
async fn send_tool_results(
|
|
258
261
|
&self,
|
|
259
262
|
results: Vec<ToolResult>,
|
|
260
|
-
tools:
|
|
263
|
+
tools: &[ToolSpec],
|
|
264
|
+
allow_tool_calls: bool,
|
|
261
265
|
sink: Option<&TokenSink>,
|
|
262
266
|
) -> Result<TurnResult, BackendError> {
|
|
263
267
|
let onde_results: Vec<onde::inference::ToolResult> = results
|
|
@@ -268,10 +272,10 @@ impl InferenceBackend for LocalBackend {
|
|
|
268
272
|
})
|
|
269
273
|
.collect();
|
|
270
274
|
|
|
271
|
-
//
|
|
272
|
-
//
|
|
275
|
+
// A forced-text round is the only round onde can stream, since no
|
|
276
|
+
// further tool calls are parsed.
|
|
273
277
|
if let Some(sink) = sink
|
|
274
|
-
&&
|
|
278
|
+
&& !allow_tool_calls
|
|
275
279
|
{
|
|
276
280
|
let rx = self
|
|
277
281
|
.engine
|
|
@@ -281,7 +285,7 @@ impl InferenceBackend for LocalBackend {
|
|
|
281
285
|
return drain_onde_stream(rx, sink).await;
|
|
282
286
|
}
|
|
283
287
|
|
|
284
|
-
let onde_tools =
|
|
288
|
+
let onde_tools = allow_tool_calls.then(|| to_onde_tools(tools));
|
|
285
289
|
let result = self
|
|
286
290
|
.engine
|
|
287
291
|
.send_tool_results(onde_results, onde_tools.as_deref())
|
|
@@ -485,12 +489,14 @@ impl OpenAiBackend {
|
|
|
485
489
|
.collect()
|
|
486
490
|
}
|
|
487
491
|
|
|
488
|
-
/// POST the current history
|
|
489
|
-
///
|
|
490
|
-
/// is
|
|
492
|
+
/// POST the current history and apply the assistant reply to history.
|
|
493
|
+
/// `tools` is always the known catalog, while `allow_tool_calls` determines
|
|
494
|
+
/// whether it is advertised to the model and whether returned calls may run.
|
|
495
|
+
/// Streams via SSE when `sink` is set; otherwise reads a single JSON response.
|
|
491
496
|
async fn complete(
|
|
492
497
|
&self,
|
|
493
|
-
tools:
|
|
498
|
+
tools: &[ToolSpec],
|
|
499
|
+
allow_tool_calls: bool,
|
|
494
500
|
sink: Option<&TokenSink>,
|
|
495
501
|
) -> Result<TurnResult, BackendError> {
|
|
496
502
|
let url = format!("{}/chat/completions", self.base_url.trim_end_matches('/'));
|
|
@@ -501,9 +507,7 @@ impl OpenAiBackend {
|
|
|
501
507
|
"messages": *self.history.lock().await,
|
|
502
508
|
"stream": streaming,
|
|
503
509
|
});
|
|
504
|
-
if
|
|
505
|
-
&& !tools.is_empty()
|
|
506
|
-
{
|
|
510
|
+
if allow_tool_calls && !tools.is_empty() {
|
|
507
511
|
body["tools"] = serde_json::Value::Array(Self::tools_json(tools));
|
|
508
512
|
// OpenAI specifies `auto` as the default when tools are present,
|
|
509
513
|
// but not every OpenAI-compatible gateway implements that default.
|
|
@@ -528,13 +532,11 @@ impl OpenAiBackend {
|
|
|
528
532
|
return Err(describe_api_error(status, &body));
|
|
529
533
|
}
|
|
530
534
|
|
|
531
|
-
// The specs are needed downstream to type the arguments of any tool
|
|
532
|
-
// call the model emitted as text rather than as a structured call.
|
|
533
|
-
let tools = tools.unwrap_or(&[]);
|
|
534
535
|
if let Some(sink) = sink {
|
|
535
|
-
self.consume_stream(response, sink, tools)
|
|
536
|
+
self.consume_stream(response, sink, tools, allow_tool_calls)
|
|
537
|
+
.await
|
|
536
538
|
} else {
|
|
537
|
-
self.consume_json(response, tools).await
|
|
539
|
+
self.consume_json(response, tools, allow_tool_calls).await
|
|
538
540
|
}
|
|
539
541
|
}
|
|
540
542
|
|
|
@@ -543,6 +545,7 @@ impl OpenAiBackend {
|
|
|
543
545
|
&self,
|
|
544
546
|
response: reqwest::Response,
|
|
545
547
|
tools: &[ToolSpec],
|
|
548
|
+
allow_tool_calls: bool,
|
|
546
549
|
) -> Result<TurnResult, BackendError> {
|
|
547
550
|
let parsed: ChatCompletion = response
|
|
548
551
|
.json()
|
|
@@ -556,7 +559,7 @@ impl OpenAiBackend {
|
|
|
556
559
|
.map(|choice| choice.message)
|
|
557
560
|
.ok_or_else(|| "endpoint returned no choices".to_string())?;
|
|
558
561
|
|
|
559
|
-
let text = message.content.clone().unwrap_or_default();
|
|
562
|
+
let mut text = message.content.clone().unwrap_or_default();
|
|
560
563
|
let tool_calls: Vec<ToolCall> = message
|
|
561
564
|
.tool_calls
|
|
562
565
|
.iter()
|
|
@@ -568,6 +571,31 @@ impl OpenAiBackend {
|
|
|
568
571
|
})
|
|
569
572
|
.collect();
|
|
570
573
|
|
|
574
|
+
if !allow_tool_calls {
|
|
575
|
+
let (cleaned, recovered) = crate::inline_tool_calls::extract(&text, tools);
|
|
576
|
+
text = cleaned;
|
|
577
|
+
let suppressed = tool_calls.len() + recovered.len();
|
|
578
|
+
if suppressed > 0 {
|
|
579
|
+
let names = tool_calls
|
|
580
|
+
.iter()
|
|
581
|
+
.map(|call| call.name.as_str())
|
|
582
|
+
.chain(recovered.iter().map(|call| call.name.as_str()))
|
|
583
|
+
.collect::<Vec<_>>()
|
|
584
|
+
.join(", ");
|
|
585
|
+
log::warn!(
|
|
586
|
+
"suppressed {suppressed} tool call(s) from a forced-text response: {names}"
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
self.history
|
|
590
|
+
.lock()
|
|
591
|
+
.await
|
|
592
|
+
.push(streamed_assistant_history(&text, &[]));
|
|
593
|
+
return Ok(TurnResult {
|
|
594
|
+
text,
|
|
595
|
+
tool_calls: Vec::new(),
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
571
599
|
// Some models write a tool call out as literal `<tool_call>` text
|
|
572
600
|
// instead of using the structured field (see `inline_tool_calls`).
|
|
573
601
|
// Recover it, or the turn ends with the tag rendered as prose and
|
|
@@ -617,6 +645,7 @@ impl OpenAiBackend {
|
|
|
617
645
|
response: reqwest::Response,
|
|
618
646
|
sink: &TokenSink,
|
|
619
647
|
tools: &[ToolSpec],
|
|
648
|
+
allow_tool_calls: bool,
|
|
620
649
|
) -> Result<TurnResult, BackendError> {
|
|
621
650
|
use futures::StreamExt;
|
|
622
651
|
|
|
@@ -691,10 +720,12 @@ impl OpenAiBackend {
|
|
|
691
720
|
}
|
|
692
721
|
}
|
|
693
722
|
crate::inline_tool_calls::ScanEvent::ToolCall(call) => {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
723
|
+
if allow_tool_calls {
|
|
724
|
+
log::warn!(
|
|
725
|
+
"recovered tool call '{}' the model emitted as text instead of a structured call",
|
|
726
|
+
call.name
|
|
727
|
+
);
|
|
728
|
+
}
|
|
698
729
|
recovered.push(ToolCall {
|
|
699
730
|
id: format!("call_recovered_{}", recovered.len()),
|
|
700
731
|
name: call.name,
|
|
@@ -755,6 +786,19 @@ impl OpenAiBackend {
|
|
|
755
786
|
.collect();
|
|
756
787
|
tool_calls.extend(recovered);
|
|
757
788
|
|
|
789
|
+
if !allow_tool_calls && !tool_calls.is_empty() {
|
|
790
|
+
let names = tool_calls
|
|
791
|
+
.iter()
|
|
792
|
+
.map(|call| call.name.as_str())
|
|
793
|
+
.collect::<Vec<_>>()
|
|
794
|
+
.join(", ");
|
|
795
|
+
log::warn!(
|
|
796
|
+
"suppressed {} tool call(s) from a forced-text response: {names}",
|
|
797
|
+
tool_calls.len()
|
|
798
|
+
);
|
|
799
|
+
tool_calls.clear();
|
|
800
|
+
}
|
|
801
|
+
|
|
758
802
|
// Record the assistant turn so later tool results have context.
|
|
759
803
|
self.history
|
|
760
804
|
.lock()
|
|
@@ -813,13 +857,14 @@ impl InferenceBackend for OpenAiBackend {
|
|
|
813
857
|
.lock()
|
|
814
858
|
.await
|
|
815
859
|
.push(serde_json::json!({ "role": "user", "content": text }));
|
|
816
|
-
self.complete(
|
|
860
|
+
self.complete(tools, true, sink).await
|
|
817
861
|
}
|
|
818
862
|
|
|
819
863
|
async fn send_tool_results(
|
|
820
864
|
&self,
|
|
821
865
|
results: Vec<ToolResult>,
|
|
822
|
-
tools:
|
|
866
|
+
tools: &[ToolSpec],
|
|
867
|
+
allow_tool_calls: bool,
|
|
823
868
|
sink: Option<&TokenSink>,
|
|
824
869
|
) -> Result<TurnResult, BackendError> {
|
|
825
870
|
{
|
|
@@ -832,7 +877,7 @@ impl InferenceBackend for OpenAiBackend {
|
|
|
832
877
|
}));
|
|
833
878
|
}
|
|
834
879
|
}
|
|
835
|
-
self.complete(tools, sink).await
|
|
880
|
+
self.complete(tools, allow_tool_calls, sink).await
|
|
836
881
|
}
|
|
837
882
|
|
|
838
883
|
async fn record_cancelled_tool_results(&self, results: Vec<ToolResult>) {
|
|
@@ -887,7 +932,7 @@ impl InferenceBackend for OpenAiBackend {
|
|
|
887
932
|
}));
|
|
888
933
|
*self.history.lock().await = request;
|
|
889
934
|
|
|
890
|
-
let summary = match self.complete(
|
|
935
|
+
let summary = match self.complete(&[], false, None).await {
|
|
891
936
|
Ok(result) => result.text,
|
|
892
937
|
Err(error) => {
|
|
893
938
|
// Roll back the summarization request; the turn never happened.
|
|
@@ -3346,12 +3346,8 @@ mod tui {
|
|
|
3346
3346
|
|
|
3347
3347
|
// on the last round, pass no tools so the model must produce text —
|
|
3348
3348
|
// that's also the round we can stream on-device.
|
|
3349
|
-
let
|
|
3350
|
-
|
|
3351
|
-
} else {
|
|
3352
|
-
None
|
|
3353
|
-
};
|
|
3354
|
-
let sink = if next_tools.is_none() {
|
|
3349
|
+
let allow_tool_calls = round < MAX_TOOL_ROUNDS;
|
|
3350
|
+
let sink = if !allow_tool_calls {
|
|
3355
3351
|
streamed = true;
|
|
3356
3352
|
Some(&delta_tx)
|
|
3357
3353
|
} else {
|
|
@@ -3359,7 +3355,7 @@ mod tui {
|
|
|
3359
3355
|
};
|
|
3360
3356
|
|
|
3361
3357
|
match backend
|
|
3362
|
-
.send_tool_results(tool_results,
|
|
3358
|
+
.send_tool_results(tool_results, &tools, allow_tool_calls, sink)
|
|
3363
3359
|
.await
|
|
3364
3360
|
{
|
|
3365
3361
|
Ok(r) => result = r,
|
|
@@ -170,7 +170,7 @@ pub async fn run(config: HeadlessConfig) -> i32 {
|
|
|
170
170
|
if settings::local_inference_enabled() {
|
|
171
171
|
None
|
|
172
172
|
} else {
|
|
173
|
-
provider::cloud_tier_provider(
|
|
173
|
+
provider::cloud_tier_provider(provider::DEFAULT_CLOUD_TIER)
|
|
174
174
|
}
|
|
175
175
|
});
|
|
176
176
|
let Some(cfg) = provider_cfg else {
|
|
@@ -304,18 +304,14 @@ async fn run_prompt(
|
|
|
304
304
|
});
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
let
|
|
308
|
-
Some(tools.as_slice())
|
|
309
|
-
} else {
|
|
310
|
-
None // last round: force text
|
|
311
|
-
};
|
|
307
|
+
let allow_tool_calls = round < crate::MAX_TOOL_ROUNDS;
|
|
312
308
|
|
|
313
309
|
// Whatever this round says starts a new paragraph rather than
|
|
314
310
|
// continuing the sentence the tool calls interrupted.
|
|
315
311
|
reply.interrupt();
|
|
316
312
|
|
|
317
313
|
result = drain_to_stdout(
|
|
318
|
-
backend.send_tool_results(tool_results,
|
|
314
|
+
backend.send_tool_results(tool_results, &tools, allow_tool_calls, sink_opt),
|
|
319
315
|
&mut sink_rx,
|
|
320
316
|
&mut reply,
|
|
321
317
|
)
|