sigit-code 1.5.9__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.9 → sigit_code-1.5.10}/CHANGELOG.md +36 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/Cargo.lock +1 -1
- {sigit_code-1.5.9 → sigit_code-1.5.10}/Cargo.toml +1 -1
- {sigit_code-1.5.9 → sigit_code-1.5.10}/PKG-INFO +1 -1
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/backend.rs +74 -29
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/chat.rs +3 -7
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/headless.rs +3 -7
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/inline_tool_calls.rs +104 -10
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/main.rs +53 -10
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/provider.rs +8 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/tools.rs +2 -6
- {sigit_code-1.5.9 → sigit_code-1.5.10}/tests/acp_permissions.rs +114 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.agents/AGENTS.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.agents/skills/agent-client-protocol/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.agents/skills/ai-assisted-coding/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.agents/skills/branding/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.agents/skills/run-sigit/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.agents/skills/run-sigit/driver.mjs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.agents/skills/run-sigit/tui-smoke.sh +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.agents/skills/sigit-code-release/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.agents/skills/tool-calling/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.claude/skills/agent-client-protocol/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.claude/skills/ai-assisted-coding/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.claude/skills/branding/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.claude/skills/run-sigit/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.claude/skills/run-sigit/driver.mjs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.claude/skills/run-sigit/tui-smoke.sh +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.claude/skills/sigit-code-release/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.claude/skills/tool-calling/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.github/workflows/ci.yml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.github/workflows/release-aur.yml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.github/workflows/release-crates.yml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.github/workflows/release-github.yml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.github/workflows/release-homebrew.yml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.github/workflows/release-npm.yml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.github/workflows/release-nuget.yml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.github/workflows/release-pypi.yml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.github/workflows/release-scoop.yml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.github/workflows/release-winget.yml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.gitignore +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/.nvmrc +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/AGENTS.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/CLAUDE.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/LICENSE +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/README.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/docs/hooks.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/docs/mcp.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/examples/settings-with-hooks.toml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/examples/skills/README.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/examples/skills/commit-message/SKILL.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/npm/README.md.tmpl +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/npm/package-compat.json.tmpl +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/npm/package-main.json.tmpl +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/npm/package.json.tmpl +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/npm/scripts/render-main-package.cjs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/npm/scripts/render-platform-package.cjs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/npm/sigit/.gitignore +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/npm/sigit/README.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/npm/sigit/package.json +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/npm/sigit/src/index.ts +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/npm/sigit/tsconfig.json +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/nuget/.gitignore +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/nuget/sigit/Program.cs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/nuget/sigit/README.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/nuget/sigit/SiGit.Code.csproj +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/packaging/aur/PKGBUILD.in +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/packaging/nfpm.yaml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/packaging/winget/getSigit.siGitCode.installer.yaml.in +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/packaging/winget/getSigit.siGitCode.locale.en-US.yaml.in +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/packaging/winget/getSigit.siGitCode.yaml.in +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/pypi/README.md +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/pypi/pyproject.toml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/pyproject.toml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/rust-toolchain.toml +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/account.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/browser_auth.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/commands.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/credentials.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/frontmatter.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/hooks.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/instructions.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/mcp.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/models.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/permissions.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/session_store.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/settings.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/setup.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/skills.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/subagents.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/src/workspace.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/tests/acp_endpoint_errors.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/tests/acp_multi_root.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/tests/acp_session_load.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/tests/acp_tool_stdin.rs +0 -0
- {sigit_code-1.5.9 → sigit_code-1.5.10}/tests/headless_mode.rs +0 -0
|
@@ -1,5 +1,41 @@
|
|
|
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
|
+
|
|
3
39
|
## 1.5.9
|
|
4
40
|
|
|
5
41
|
### Fixed
|
|
@@ -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
|
)
|
|
@@ -83,16 +83,22 @@ pub fn extract(text: &str, tools: &[ToolSpec]) -> (String, Vec<Recovered>) {
|
|
|
83
83
|
fn parse_xml_block(inner: &str, tools: &[ToolSpec]) -> Option<Recovered> {
|
|
84
84
|
let key_idx = inner.find("<arg_key>");
|
|
85
85
|
let malformed_key_idx = inner.find(XML_OPEN_TAG);
|
|
86
|
-
let
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
&inner[
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
let check_status_name = key_idx.and_then(|idx| parse_check_status_preamble(&inner[..idx]));
|
|
87
|
+
let (name, mut rest, malformed_first_key) =
|
|
88
|
+
if let (Some(idx), Some(name)) = (key_idx, check_status_name) {
|
|
89
|
+
(name, &inner[idx..], false)
|
|
90
|
+
} else {
|
|
91
|
+
match (key_idx, malformed_key_idx) {
|
|
92
|
+
(Some(key_idx), Some(malformed_key_idx)) if malformed_key_idx < key_idx => (
|
|
93
|
+
inner[..malformed_key_idx].trim(),
|
|
94
|
+
&inner[malformed_key_idx..],
|
|
95
|
+
true,
|
|
96
|
+
),
|
|
97
|
+
(Some(idx), _) => (inner[..idx].trim(), &inner[idx..], false),
|
|
98
|
+
(None, Some(idx)) => (inner[..idx].trim(), &inner[idx..], true),
|
|
99
|
+
(None, None) => (inner.trim(), "", false),
|
|
100
|
+
}
|
|
101
|
+
};
|
|
96
102
|
if name.is_empty() || name.contains(['<', '>']) || !offered_tool(tools, name) {
|
|
97
103
|
return None;
|
|
98
104
|
}
|
|
@@ -132,6 +138,24 @@ fn parse_xml_block(inner: &str, tools: &[ToolSpec]) -> Option<Recovered> {
|
|
|
132
138
|
})
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
/// Some GLM-family responses prefix the real arguments with generation-control
|
|
142
|
+
/// metadata shaped like `tool_name CheckStatus=value</arg_value>`. It is not a
|
|
143
|
+
/// tool argument: accept that exact bounded preamble and discard it, while
|
|
144
|
+
/// leaving every other malformed prefix untouched.
|
|
145
|
+
fn parse_check_status_preamble(prefix: &str) -> Option<&str> {
|
|
146
|
+
let (name, status) = prefix.trim().split_once(" CheckStatus=")?;
|
|
147
|
+
let status = status.strip_suffix("</arg_value>")?;
|
|
148
|
+
if name.is_empty()
|
|
149
|
+
|| status.is_empty()
|
|
150
|
+
|| !status
|
|
151
|
+
.chars()
|
|
152
|
+
.all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
|
|
153
|
+
{
|
|
154
|
+
return None;
|
|
155
|
+
}
|
|
156
|
+
Some(name)
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
fn parse_k3_tools_block(inner: &str, tools: &[ToolSpec]) -> Option<Vec<Recovered>> {
|
|
136
160
|
let mut calls = Vec::new();
|
|
137
161
|
let mut rest = inner;
|
|
@@ -565,6 +589,47 @@ mod tests {
|
|
|
565
589
|
assert_eq!(args["cwd"], "/tmp");
|
|
566
590
|
}
|
|
567
591
|
|
|
592
|
+
#[test]
|
|
593
|
+
fn recovers_glm_call_with_check_status_before_real_arguments() {
|
|
594
|
+
let tools = vec![command_output_spec()];
|
|
595
|
+
let (text, calls) = extract(
|
|
596
|
+
"polling <tool_call>command_output CheckStatus=true_or_poll_again_with_different_params</arg_value><arg_key>task_id</arg_key><arg_value>1</arg_value></tool_call>",
|
|
597
|
+
&tools,
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
assert_eq!(text, "polling ");
|
|
601
|
+
assert_eq!(calls.len(), 1);
|
|
602
|
+
assert_eq!(calls[0].name, "command_output");
|
|
603
|
+
let args: serde_json::Value = serde_json::from_str(&calls[0].arguments).unwrap();
|
|
604
|
+
assert_eq!(args["task_id"], 1);
|
|
605
|
+
assert!(args["task_id"].is_number());
|
|
606
|
+
assert!(args.get("CheckStatus").is_none());
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
#[test]
|
|
610
|
+
fn malformed_check_status_preamble_is_left_as_text() {
|
|
611
|
+
let text = "<tool_call>command_output CheckStatus=keep going</arg_value><arg_key>task_id</arg_key><arg_value>1</arg_value></tool_call>";
|
|
612
|
+
let (out, calls) = extract(text, &[command_output_spec()]);
|
|
613
|
+
assert_eq!(out, text);
|
|
614
|
+
assert!(calls.is_empty());
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#[test]
|
|
618
|
+
fn check_status_after_a_real_argument_is_left_as_text() {
|
|
619
|
+
let text = "<tool_call>command_output<arg_key>task_id</arg_key><arg_value>1</arg_value> CheckStatus=true_or_poll_again_with_different_params</arg_value></tool_call>";
|
|
620
|
+
let (out, calls) = extract(text, &[command_output_spec()]);
|
|
621
|
+
assert_eq!(out, text);
|
|
622
|
+
assert!(calls.is_empty());
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#[test]
|
|
626
|
+
fn check_status_for_an_unknown_tool_is_left_as_text() {
|
|
627
|
+
let text = "<tool_call>not_offered CheckStatus=true_or_poll_again_with_different_params</arg_value><arg_key>task_id</arg_key><arg_value>1</arg_value></tool_call>";
|
|
628
|
+
let (out, calls) = extract(text, &[command_output_spec()]);
|
|
629
|
+
assert_eq!(out, text);
|
|
630
|
+
assert!(calls.is_empty());
|
|
631
|
+
}
|
|
632
|
+
|
|
568
633
|
#[test]
|
|
569
634
|
fn unknown_glm_tool_is_left_as_text() {
|
|
570
635
|
let text = "<tool_call>not_offered<tool_call>command</arg_key><arg_value>pwd</arg_value></tool_call>";
|
|
@@ -773,6 +838,35 @@ mod tests {
|
|
|
773
838
|
assert_eq!(args["command"], "pwd");
|
|
774
839
|
}
|
|
775
840
|
|
|
841
|
+
#[test]
|
|
842
|
+
fn scanner_recovers_check_status_call_split_across_chunks() {
|
|
843
|
+
let tools = vec![command_output_spec()];
|
|
844
|
+
let mut scanner = StreamScanner::new(&tools);
|
|
845
|
+
let mut text = String::new();
|
|
846
|
+
let mut calls = Vec::new();
|
|
847
|
+
|
|
848
|
+
for chunk in [
|
|
849
|
+
"before <tool_call>command_output CheckStatus=true_or_poll_",
|
|
850
|
+
"again_with_different_params</arg_",
|
|
851
|
+
"value><arg_key>task_id</arg_key><arg_value>1</arg_value></tool_",
|
|
852
|
+
"call> after",
|
|
853
|
+
] {
|
|
854
|
+
for event in scanner.push(chunk) {
|
|
855
|
+
match event {
|
|
856
|
+
ScanEvent::Text(value) => text.push_str(&value),
|
|
857
|
+
ScanEvent::ToolCall(call) => calls.push(call),
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if let Some(rest) = scanner.take_pending() {
|
|
862
|
+
text.push_str(&rest);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
assert_eq!(text, "before after");
|
|
866
|
+
assert_eq!(calls.len(), 1);
|
|
867
|
+
assert_eq!(calls[0].name, "command_output");
|
|
868
|
+
}
|
|
869
|
+
|
|
776
870
|
#[test]
|
|
777
871
|
fn scanner_recovers_kimi_k3_tools_split_across_chunk_boundaries() {
|
|
778
872
|
let tools = vec![run_command_spec()];
|
|
@@ -1733,7 +1733,15 @@ impl SiGitAgent {
|
|
|
1733
1733
|
return Ok(PromptResponse::new(StopReason::EndTurn));
|
|
1734
1734
|
}
|
|
1735
1735
|
|
|
1736
|
-
let
|
|
1736
|
+
let slash_command = parse_slash_from_prompt_texts(args.prompt.iter().filter_map(|block| {
|
|
1737
|
+
if let ContentBlock::Text(text) = block {
|
|
1738
|
+
Some(text.text.as_str())
|
|
1739
|
+
} else {
|
|
1740
|
+
None
|
|
1741
|
+
}
|
|
1742
|
+
}));
|
|
1743
|
+
|
|
1744
|
+
let user_text = match slash_command {
|
|
1737
1745
|
// `/init` is not a status command: it substitutes the canned
|
|
1738
1746
|
// AGENTS.md-generation prompt and runs a normal agent turn, so the
|
|
1739
1747
|
// exploration and file write go through the ordinary tools and
|
|
@@ -2036,11 +2044,7 @@ impl SiGitAgent {
|
|
|
2036
2044
|
});
|
|
2037
2045
|
}
|
|
2038
2046
|
|
|
2039
|
-
let
|
|
2040
|
-
Some(tools.as_slice())
|
|
2041
|
-
} else {
|
|
2042
|
-
None // last round: force text
|
|
2043
|
-
};
|
|
2047
|
+
let allow_tool_calls = round < MAX_TOOL_ROUNDS && !force_text;
|
|
2044
2048
|
|
|
2045
2049
|
// Whatever this round says starts a new paragraph rather than
|
|
2046
2050
|
// continuing the sentence the tool calls interrupted.
|
|
@@ -2050,7 +2054,7 @@ impl SiGitAgent {
|
|
|
2050
2054
|
.drain_turn(
|
|
2051
2055
|
cx,
|
|
2052
2056
|
&session_id,
|
|
2053
|
-
backend.send_tool_results(tool_results,
|
|
2057
|
+
backend.send_tool_results(tool_results, &tools, allow_tool_calls, Some(&sink)),
|
|
2054
2058
|
&mut sink_rx,
|
|
2055
2059
|
&mut reply,
|
|
2056
2060
|
)
|
|
@@ -2248,7 +2252,11 @@ impl SiGitAgent {
|
|
|
2248
2252
|
if settings::local_inference_enabled() {
|
|
2249
2253
|
return;
|
|
2250
2254
|
}
|
|
2251
|
-
if self
|
|
2255
|
+
if self
|
|
2256
|
+
.switch_to_cloud_tier(provider::DEFAULT_CLOUD_TIER)
|
|
2257
|
+
.await
|
|
2258
|
+
.is_some()
|
|
2259
|
+
{
|
|
2252
2260
|
log::info!("startup: local inference off; routing inference to siGit Code Cloud");
|
|
2253
2261
|
} else {
|
|
2254
2262
|
log::warn!(
|
|
@@ -3032,6 +3040,15 @@ fn parse_slash(input: &str) -> Option<SlashCommand> {
|
|
|
3032
3040
|
})
|
|
3033
3041
|
}
|
|
3034
3042
|
|
|
3043
|
+
/// ACP clients may prepend context as separate text blocks before the user's
|
|
3044
|
+
/// input. Search from the end so a standalone slash command in the final user
|
|
3045
|
+
/// block is still dispatched locally instead of being buried in joined context.
|
|
3046
|
+
fn parse_slash_from_prompt_texts<'a>(
|
|
3047
|
+
texts: impl DoubleEndedIterator<Item = &'a str>,
|
|
3048
|
+
) -> Option<SlashCommand> {
|
|
3049
|
+
texts.rev().find_map(parse_slash)
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3035
3052
|
/// `on`/`off` (and synonyms) → `Some(bool)`; missing or unrecognized → `None`
|
|
3036
3053
|
/// (meaning "toggle the current value").
|
|
3037
3054
|
fn parse_on_off(arg: Option<&str>) -> Option<bool> {
|
|
@@ -3645,7 +3662,7 @@ async fn run_interactive(tty: std::fs::File, mut cleanup_tty: std::fs::File) ->
|
|
|
3645
3662
|
let cloud_when_off = if settings::local_inference_enabled() {
|
|
3646
3663
|
None
|
|
3647
3664
|
} else {
|
|
3648
|
-
provider::cloud_tier_provider(
|
|
3665
|
+
provider::cloud_tier_provider(provider::DEFAULT_CLOUD_TIER)
|
|
3649
3666
|
};
|
|
3650
3667
|
|
|
3651
3668
|
match cloud_when_off {
|
|
@@ -4176,7 +4193,19 @@ mod tests {
|
|
|
4176
4193
|
|
|
4177
4194
|
#[test]
|
|
4178
4195
|
fn history_replay_redraws_the_conversation_for_the_client() {
|
|
4179
|
-
|
|
4196
|
+
// A fixed path, not `std::env::current_dir()`: the cwd is process-global
|
|
4197
|
+
// and a handful of other tests (skills/commands/subagents discovery,
|
|
4198
|
+
// the subagent end-to-end test) briefly `set_current_dir` to a temp
|
|
4199
|
+
// directory behind `ENV_TEST_LOCK`. This test asserts on the exact
|
|
4200
|
+
// rendered title, so racing one of them mid-run doesn't just read a
|
|
4201
|
+
// stale value — it can hand back a temp path long enough to trip the
|
|
4202
|
+
// title's truncation, breaking an assertion that has nothing to do
|
|
4203
|
+
// with the cwd. A literal path sidesteps the shared state entirely.
|
|
4204
|
+
let changelog_path = if cfg!(windows) {
|
|
4205
|
+
PathBuf::from(r"C:\repo\CHANGELOG.md")
|
|
4206
|
+
} else {
|
|
4207
|
+
PathBuf::from("/repo/CHANGELOG.md")
|
|
4208
|
+
};
|
|
4180
4209
|
let changelog_path_text = changelog_path.to_string_lossy().into_owned();
|
|
4181
4210
|
let arguments = serde_json::json!({ "path": changelog_path_text }).to_string();
|
|
4182
4211
|
let history = vec![
|
|
@@ -4411,6 +4440,20 @@ mod tests {
|
|
|
4411
4440
|
assert!(matches!(parse_slash(" /init "), Some(SlashCommand::Init)));
|
|
4412
4441
|
}
|
|
4413
4442
|
|
|
4443
|
+
#[test]
|
|
4444
|
+
fn parse_slash_finds_command_after_client_context_blocks() {
|
|
4445
|
+
let texts = [
|
|
4446
|
+
"<system-reminder>Xcode context</system-reminder>",
|
|
4447
|
+
"Project structure: App/App.swift",
|
|
4448
|
+
"/models",
|
|
4449
|
+
];
|
|
4450
|
+
|
|
4451
|
+
assert!(matches!(
|
|
4452
|
+
parse_slash_from_prompt_texts(texts.into_iter()),
|
|
4453
|
+
Some(SlashCommand::Models(None))
|
|
4454
|
+
));
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4414
4457
|
#[test]
|
|
4415
4458
|
fn system_prompt_points_issue_and_pr_work_at_the_official_mcp_tools() {
|
|
4416
4459
|
// The guidance must reference the exact namespaced names the official
|
|
@@ -43,6 +43,8 @@ pub const CLOUD_TIERS: &[&str] = &[
|
|
|
43
43
|
"nova",
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
+
pub const DEFAULT_CLOUD_TIER: &str = "nova";
|
|
47
|
+
|
|
46
48
|
/// Base URL of the siGit Code Cloud inference endpoint. Override with
|
|
47
49
|
/// `SIGIT_CLOUD_URL` (dev: `http://localhost:8090/v1`).
|
|
48
50
|
pub fn cloud_base_url() -> String {
|
|
@@ -259,4 +261,10 @@ mod tests {
|
|
|
259
261
|
assert_eq!(non_empty(Some(" x ".to_string())), Some("x".to_string()));
|
|
260
262
|
assert_eq!(non_empty(None), None);
|
|
261
263
|
}
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn maps_default_cloud_tier_to_wire_model_id() {
|
|
267
|
+
assert_eq!(DEFAULT_CLOUD_TIER, "nova");
|
|
268
|
+
assert_eq!(tier_to_model(DEFAULT_CLOUD_TIER), "onde-nova");
|
|
269
|
+
}
|
|
262
270
|
}
|
|
@@ -939,13 +939,9 @@ async fn run_subagent(backend: &dyn InferenceBackend, prompt: &str, allowed: &[S
|
|
|
939
939
|
}
|
|
940
940
|
|
|
941
941
|
// On the last round, offer no tools so the model must produce text.
|
|
942
|
-
let
|
|
943
|
-
Some(specs.as_slice())
|
|
944
|
-
} else {
|
|
945
|
-
None
|
|
946
|
-
};
|
|
942
|
+
let allow_tool_calls = round < SUBAGENT_MAX_TOOL_ROUNDS;
|
|
947
943
|
result = match backend
|
|
948
|
-
.send_tool_results(tool_results,
|
|
944
|
+
.send_tool_results(tool_results, &specs, allow_tool_calls, None)
|
|
949
945
|
.await
|
|
950
946
|
{
|
|
951
947
|
Ok(r) => r,
|
|
@@ -943,6 +943,120 @@ fn a_tool_call_emitted_as_text_is_executed_rather_than_rendered() {
|
|
|
943
943
|
let _ = std::fs::remove_dir_all(&scratch);
|
|
944
944
|
}
|
|
945
945
|
|
|
946
|
+
/// Once the repetition guard removes tools from a request, a model may still
|
|
947
|
+
/// emit its learned tool syntax. Known calls must be removed from the visible
|
|
948
|
+
/// response without being executed or recorded as orphaned history entries.
|
|
949
|
+
#[test]
|
|
950
|
+
fn forced_text_suppresses_glm_check_status_tool_markup() {
|
|
951
|
+
let repeated_arguments = json!({"task_id": 1}).to_string();
|
|
952
|
+
let endpoint = start_fake_endpoint(vec![
|
|
953
|
+
sse_tool_call("call_1", "command_output", &repeated_arguments),
|
|
954
|
+
sse_tool_call("call_2", "command_output", &repeated_arguments),
|
|
955
|
+
sse_tool_call("call_3", "command_output", &repeated_arguments),
|
|
956
|
+
sse_body(&[
|
|
957
|
+
json!({"choices": [{"delta": {"content": "Build is still running. <tool_call>command_output CheckStatus=true_or_poll_"}}]}),
|
|
958
|
+
json!({"choices": [{"delta": {"content": "again_with_different_params</arg_value><arg_key>task_id</arg_key><arg_value>1</arg_value></tool_call>"}}]}),
|
|
959
|
+
// Also enforce the forced-text boundary for a structured call.
|
|
960
|
+
json!({"choices": [{"delta": {"tool_calls": [{
|
|
961
|
+
"index": 0,
|
|
962
|
+
"id": "call_4",
|
|
963
|
+
"function": {"name": "command_output", "arguments": "{\"task_id\":999}"},
|
|
964
|
+
}]}}]}),
|
|
965
|
+
]),
|
|
966
|
+
sse_text("Next turn."),
|
|
967
|
+
]);
|
|
968
|
+
|
|
969
|
+
let scratch =
|
|
970
|
+
std::env::temp_dir().join(format!("sigit_acp_forced_text_{}", std::process::id()));
|
|
971
|
+
let config_dir = scratch.join("config");
|
|
972
|
+
let cwd = scratch.join("cwd");
|
|
973
|
+
std::fs::create_dir_all(&config_dir).unwrap();
|
|
974
|
+
std::fs::create_dir_all(&cwd).unwrap();
|
|
975
|
+
|
|
976
|
+
let mut agent = spawn_agent(endpoint.port, &config_dir);
|
|
977
|
+
|
|
978
|
+
let id = agent.request(
|
|
979
|
+
"initialize",
|
|
980
|
+
json!({"protocolVersion": 1, "clientCapabilities": {}}),
|
|
981
|
+
);
|
|
982
|
+
agent.wait_for_response(id);
|
|
983
|
+
|
|
984
|
+
let id = agent.request("session/new", json!({"cwd": cwd, "mcpServers": []}));
|
|
985
|
+
let session_id = agent.wait_for_response(id)["result"]["sessionId"]
|
|
986
|
+
.as_str()
|
|
987
|
+
.expect("session id")
|
|
988
|
+
.to_string();
|
|
989
|
+
|
|
990
|
+
let prompt_id = agent.request(
|
|
991
|
+
"session/prompt",
|
|
992
|
+
json!({
|
|
993
|
+
"sessionId": session_id,
|
|
994
|
+
"prompt": [{"type": "text", "text": "keep polling"}],
|
|
995
|
+
}),
|
|
996
|
+
);
|
|
997
|
+
let (response, rendered) = agent.wait_for_prompt(prompt_id);
|
|
998
|
+
assert_eq!(response["result"]["stopReason"], "end_turn");
|
|
999
|
+
assert!(
|
|
1000
|
+
rendered.contains("Build is still running."),
|
|
1001
|
+
"surrounding prose should remain visible: {rendered:?}"
|
|
1002
|
+
);
|
|
1003
|
+
assert!(
|
|
1004
|
+
!rendered.contains("<tool_call>")
|
|
1005
|
+
&& !rendered.contains("<arg_key>")
|
|
1006
|
+
&& !rendered.contains("CheckStatus"),
|
|
1007
|
+
"forced tool markup reached the client: {rendered:?}"
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
{
|
|
1011
|
+
let requests = endpoint.requests.lock().unwrap();
|
|
1012
|
+
assert_eq!(
|
|
1013
|
+
requests.len(),
|
|
1014
|
+
4,
|
|
1015
|
+
"a forced-text tool call must not start another inference round"
|
|
1016
|
+
);
|
|
1017
|
+
assert!(
|
|
1018
|
+
requests[3].get("tools").is_none(),
|
|
1019
|
+
"the repetition guard must not advertise tools: {:?}",
|
|
1020
|
+
requests[3]
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// Start another turn so the prior assistant message is replayed and its
|
|
1025
|
+
// sanitized history shape can be inspected in the recorded request.
|
|
1026
|
+
let next_prompt_id = agent.request(
|
|
1027
|
+
"session/prompt",
|
|
1028
|
+
json!({
|
|
1029
|
+
"sessionId": session_id,
|
|
1030
|
+
"prompt": [{"type": "text", "text": "what happened?"}],
|
|
1031
|
+
}),
|
|
1032
|
+
);
|
|
1033
|
+
agent.wait_for_prompt(next_prompt_id);
|
|
1034
|
+
|
|
1035
|
+
let requests = endpoint.requests.lock().unwrap();
|
|
1036
|
+
let replayed_messages = requests[4]["messages"].as_array().expect("messages");
|
|
1037
|
+
let prior_assistant = replayed_messages
|
|
1038
|
+
.iter()
|
|
1039
|
+
.rev()
|
|
1040
|
+
.find(|message| {
|
|
1041
|
+
message["role"] == "assistant"
|
|
1042
|
+
&& message["content"]
|
|
1043
|
+
.as_str()
|
|
1044
|
+
.is_some_and(|content| content.contains("Build is still running."))
|
|
1045
|
+
})
|
|
1046
|
+
.expect("sanitized forced-text assistant response");
|
|
1047
|
+
assert!(prior_assistant.get("tool_calls").is_none());
|
|
1048
|
+
assert!(
|
|
1049
|
+
!prior_assistant["content"]
|
|
1050
|
+
.as_str()
|
|
1051
|
+
.expect("assistant content")
|
|
1052
|
+
.contains("<tool_call>")
|
|
1053
|
+
);
|
|
1054
|
+
drop(requests);
|
|
1055
|
+
|
|
1056
|
+
drop(agent);
|
|
1057
|
+
let _ = std::fs::remove_dir_all(&scratch);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
946
1060
|
/// Kimi K3 may emit tool calls in XTML content blocks even when the endpoint is
|
|
947
1061
|
/// OpenAI-compatible. The backend should recover those before ACP sees them,
|
|
948
1062
|
/// unwrap visible response text, and hide private thinking text.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sigit_code-1.5.9 → sigit_code-1.5.10}/packaging/winget/getSigit.siGitCode.installer.yaml.in
RENAMED
|
File without changes
|
{sigit_code-1.5.9 → sigit_code-1.5.10}/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
|