Skip to content

Commit e204128

Browse files
RoyLinRoyLin
authored andcommitted
feat(sdk): align Python and Node.js SessionOptions with Rust core
Add missing SessionOptions fields to both SDKs that were in the Rust core but not exposed to language bindings: **Both SDKs:** - temperature (f32/number): sampling temperature, overrides provider default - thinking_budget/thinkingBudget (usize/number): extended thinking token budget - continuation_enabled/continuationEnabled (bool): enable/disable continuation injection - max_continuation_turns/maxContinuationTurns (u32/number): max continuations per execution **Python SDK only** (were kwargs-only, now also in SessionOptions object): - planning: enable planning mode - goal_tracking: enable goal tracking - max_parse_retries: max consecutive parse errors before abort - tool_timeout_ms: per-tool execution timeout - circuit_breaker_threshold: max LLM API failures before abort Integration tests in sdk/python/examples/test_api_alignment.py and sdk/node/examples/test_api_alignment.ts verify all new fields against kimi-k2.5 via KIMI_API_KEY + KIMI_BASE_URL environment variables.
1 parent 8f103ff commit e204128

6 files changed

Lines changed: 694 additions & 7 deletions

File tree

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/**
2+
* Integration tests for Node.js SDK API alignment with Rust core.
3+
*
4+
* Tests the new SessionOptions fields added in this PR:
5+
* - temperature
6+
* - thinkingBudget
7+
* - continuationEnabled
8+
* - maxContinuationTurns
9+
*
10+
* Uses the kimi-k2.5 model. API key is read from KIMI_API_KEY environment
11+
* variable.
12+
*
13+
* Usage:
14+
* KIMI_API_KEY=sk-... npx ts-node test_api_alignment.ts
15+
*/
16+
17+
import { Agent, SessionOptions } from '@a3s-lab/code';
18+
import * as path from 'path';
19+
import { fileURLToPath } from 'url';
20+
21+
const __filename = fileURLToPath(import.meta.url);
22+
const __dirname = path.dirname(__filename);
23+
24+
// HCL config that reads API key from KIMI_API_KEY env var (see agent_kimi_k2.5.hcl)
25+
const KIMI_HCL_CONFIG = path.join(__dirname, 'agent_kimi_k2.5.hcl');
26+
27+
// ---------------------------------------------------------------------------
28+
// Test helpers
29+
// ---------------------------------------------------------------------------
30+
31+
type Status = 'PASS' | 'FAIL' | 'SKIP';
32+
const results: Array<{ name: string; status: Status; detail: string }> = [];
33+
34+
function check(name: string, condition: boolean, detail = '') {
35+
const status: Status = condition ? 'PASS' : 'FAIL';
36+
results.push({ name, status, detail });
37+
const icon = condition ? '✓' : '✗';
38+
const msg = detail ? ` ${icon} ${name}: ${detail}` : ` ${icon} ${name}`;
39+
console.log(msg);
40+
return condition;
41+
}
42+
43+
function skip(name: string, reason: string) {
44+
results.push({ name, status: 'SKIP', detail: reason });
45+
console.log(` - ${name}: SKIP (${reason})`);
46+
}
47+
48+
function section(title: string) {
49+
console.log(`\n${'='.repeat(60)}`);
50+
console.log(` ${title}`);
51+
console.log('='.repeat(60));
52+
}
53+
54+
// ---------------------------------------------------------------------------
55+
// Phase 1: SessionOptions type checks (no LLM call)
56+
// ---------------------------------------------------------------------------
57+
58+
section('Phase 1: SessionOptions field type checks');
59+
60+
// Verify new fields are accepted by TypeScript and accepted as undefined
61+
const opts: SessionOptions = {
62+
temperature: undefined,
63+
thinkingBudget: undefined,
64+
continuationEnabled: undefined,
65+
maxContinuationTurns: undefined,
66+
};
67+
check('temperature field accepted', true);
68+
check('thinkingBudget field accepted', true);
69+
check('continuationEnabled field accepted', true);
70+
check('maxContinuationTurns field accepted', true);
71+
72+
const opts2: SessionOptions = {
73+
temperature: 0.5,
74+
thinkingBudget: 8000,
75+
continuationEnabled: false,
76+
maxContinuationTurns: 5,
77+
};
78+
check('temperature value 0.5', opts2.temperature === 0.5);
79+
check('thinkingBudget value 8000', opts2.thinkingBudget === 8000);
80+
check('continuationEnabled value false', opts2.continuationEnabled === false);
81+
check('maxContinuationTurns value 5', opts2.maxContinuationTurns === 5);
82+
83+
// ---------------------------------------------------------------------------
84+
// Phase 2: Integration tests against kimi-k2.5
85+
// ---------------------------------------------------------------------------
86+
87+
const apiKey = process.env.KIMI_API_KEY;
88+
89+
async function runLiveTests(_apiKey: string) {
90+
section('Phase 2: Live integration tests (kimi-k2.5)');
91+
92+
const cwd = process.cwd();
93+
94+
// --- 2a: basic send ---
95+
console.log('\n 2a. Basic send');
96+
try {
97+
const agent = await Agent.create(KIMI_HCL_CONFIG);
98+
const session = await agent.session(cwd);
99+
const result = await session.send('Reply with exactly: HELLO');
100+
check('basic send returns result', result != null);
101+
check('basic send has text', Boolean(result?.text));
102+
check('basic send contains HELLO', (result?.text ?? '').toUpperCase().includes('HELLO'),
103+
`got: ${result?.text?.slice(0, 80)}`);
104+
} catch (e: any) {
105+
check('basic send', false, String(e));
106+
}
107+
108+
// --- 2b: temperature ---
109+
console.log('\n 2b. temperature in SessionOptions');
110+
try {
111+
const agent = await Agent.create(KIMI_HCL_CONFIG);
112+
const session = await agent.session(cwd, {
113+
model: 'openai/kimi-k2.5',
114+
temperature: 0.0,
115+
});
116+
const result = await session.send('Reply with exactly: TEMPERATURE_OK');
117+
check('temperature=0.0 accepted', result != null);
118+
check('temperature=0.0 has text', Boolean(result?.text),
119+
`got: ${result?.text?.slice(0, 80)}`);
120+
} catch (e: any) {
121+
check('temperature via SessionOptions', false, String(e));
122+
}
123+
124+
// --- 2c: continuationEnabled=false ---
125+
console.log('\n 2c. continuationEnabled=false');
126+
try {
127+
const agent = await Agent.create(KIMI_HCL_CONFIG);
128+
const session = await agent.session(cwd, { continuationEnabled: false });
129+
const result = await session.send('Reply with exactly: CONT_OK');
130+
check('continuationEnabled=false accepted', result != null);
131+
check('continuationEnabled=false has text', Boolean(result?.text),
132+
`got: ${result?.text?.slice(0, 80)}`);
133+
} catch (e: any) {
134+
check('continuationEnabled=false', false, String(e));
135+
}
136+
137+
// --- 2d: maxContinuationTurns=1 ---
138+
console.log('\n 2d. maxContinuationTurns=1');
139+
try {
140+
const agent = await Agent.create(KIMI_HCL_CONFIG);
141+
const session = await agent.session(cwd, { maxContinuationTurns: 1 });
142+
const result = await session.send('Reply with exactly: TURNS_OK');
143+
check('maxContinuationTurns=1 accepted', result != null);
144+
} catch (e: any) {
145+
check('maxContinuationTurns=1', false, String(e));
146+
}
147+
148+
// --- 2e: combined new options ---
149+
console.log('\n 2e. Combined new options');
150+
try {
151+
const agent = await Agent.create(KIMI_HCL_CONFIG);
152+
const session = await agent.session(cwd, {
153+
model: 'openai/kimi-k2.5',
154+
temperature: 0.3,
155+
continuationEnabled: true,
156+
maxContinuationTurns: 2,
157+
maxParseRetries: 3,
158+
toolTimeoutMs: 30000,
159+
circuitBreakerThreshold: 5,
160+
});
161+
const result = await session.send('Reply with exactly: COMBINED_OK');
162+
check('combined options accepted', result != null);
163+
check('combined options has text', Boolean(result?.text),
164+
`got: ${result?.text?.slice(0, 80)}`);
165+
} catch (e: any) {
166+
check('combined options', false, String(e));
167+
}
168+
}
169+
170+
// ---------------------------------------------------------------------------
171+
// Main
172+
// ---------------------------------------------------------------------------
173+
174+
async function main() {
175+
if (!apiKey) {
176+
console.log('\n SKIP: KIMI_API_KEY not set — skipping live LLM tests');
177+
skip('live LLM tests', 'KIMI_API_KEY not set');
178+
} else {
179+
await runLiveTests(apiKey);
180+
}
181+
182+
// ---------------------------------------------------------------------------
183+
// Summary
184+
// ---------------------------------------------------------------------------
185+
186+
section('Summary');
187+
188+
const total = results.length;
189+
const passed = results.filter((r) => r.status === 'PASS').length;
190+
const failed = results.filter((r) => r.status === 'FAIL').length;
191+
const skipped = results.filter((r) => r.status === 'SKIP').length;
192+
193+
console.log(`\n Total: ${total} Passed: ${passed} Failed: ${failed} Skipped: ${skipped}`);
194+
195+
if (failed > 0) {
196+
console.log('\n Failed tests:');
197+
for (const { name, status, detail } of results) {
198+
if (status === 'FAIL') {
199+
console.log(` ✗ ${name}: ${detail}`);
200+
}
201+
}
202+
}
203+
204+
console.log();
205+
process.exit(failed === 0 ? 0 : 1);
206+
}
207+
208+
main().catch((e) => {
209+
console.error(`Unexpected error: ${e}`);
210+
process.exit(1);
211+
});

sdk/node/index.d.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,23 @@ export interface SessionOptions {
182182
inlineSkills?: Array<InlineSkill>
183183
/** Override maximum number of tool-call rounds for this session. */
184184
maxToolRounds?: number
185+
/**
186+
* Sampling temperature (0.0–1.0). Overrides the provider default.
187+
* Only applied when `model` is also set.
188+
*/
189+
temperature?: number
190+
/**
191+
* Extended thinking token budget (e.g. 10_000). Enables chain-of-thought reasoning.
192+
* Only applied when `model` is also set. Provider must support extended thinking.
193+
*/
194+
thinkingBudget?: number
195+
/**
196+
* Enable continuation injection (default: true).
197+
* When enabled, the loop injects a follow-up prompt when the LLM stops without completing.
198+
*/
199+
continuationEnabled?: boolean
200+
/** Maximum continuation injections per execution (default: 3). */
201+
maxContinuationTurns?: number
185202
/**
186203
* Session ID (auto-generated if not set).
187204
*
@@ -488,6 +505,8 @@ export interface SubAgentConfig {
488505
workspace?: string
489506
/** Extra directories to scan for agent definition files */
490507
agentDirs?: Array<string>
508+
/** Extra directories to scan for skill definition files */
509+
skillDirs?: Array<string>
491510
/**
492511
* Lane queue config for External/Hybrid tool dispatch.
493512
* When set, tools in the specified lanes are routed to external workers.
@@ -523,6 +542,8 @@ export interface AgentSlot {
523542
workspace?: string
524543
/** Extra directories to scan for agent definition files */
525544
agentDirs?: Array<string>
545+
/** Extra directories to scan for skill definition files */
546+
skillDirs?: Array<string>
526547
/** Lane queue config for External/Hybrid tool dispatch */
527548
laneConfig?: SessionQueueConfig
528549
}
@@ -574,8 +595,6 @@ export declare class EventStream {
574595
* When `done` is true, the stream is exhausted.
575596
*/
576597
next(): Promise<NextResult>
577-
/** Async iterator protocol support for `for await...of` loops */
578-
[Symbol.asyncIterator](): EventStream
579598
}
580599
/**
581600
* File-backed long-term memory store.

sdk/node/index.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,6 @@ if (!nativeBinding) {
312312

313313
const { EventStream, FileMemoryStore, FileSessionStore, MemorySessionStore, DefaultSecurityProvider, DocumentParserRegistry, StdioTransport, HttpTransport, WebSocketTransport, UnixSocketTransport, Agent, Session, builtinSkills, TeamTaskBoard, Team, TeamRunner, SubAgentHandle, Orchestrator } = nativeBinding
314314

315-
// Add async iterator support to EventStream
316-
EventStream.prototype[Symbol.asyncIterator] = function() {
317-
return this
318-
}
319-
320315
module.exports.EventStream = EventStream
321316
module.exports.FileMemoryStore = FileMemoryStore
322317
module.exports.FileSessionStore = FileSessionStore

sdk/node/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,17 @@ pub struct SessionOptions {
734734
pub inline_skills: Option<Vec<InlineSkill>>,
735735
/// Override maximum number of tool-call rounds for this session.
736736
pub max_tool_rounds: Option<u32>,
737+
/// Sampling temperature (0.0–1.0). Overrides the provider default.
738+
/// Only applied when `model` is also set.
739+
pub temperature: Option<f64>,
740+
/// Extended thinking token budget (e.g. 10_000). Enables chain-of-thought reasoning.
741+
/// Only applied when `model` is also set. Provider must support extended thinking.
742+
pub thinking_budget: Option<u32>,
743+
/// Enable continuation injection (default: true).
744+
/// When enabled, the loop injects a follow-up prompt when the LLM stops without completing.
745+
pub continuation_enabled: Option<bool>,
746+
/// Maximum continuation injections per execution (default: 3).
747+
pub max_continuation_turns: Option<u32>,
737748
/// Session ID (auto-generated if not set).
738749
///
739750
/// Set a stable ID so the session can be saved and resumed later:
@@ -1069,6 +1080,18 @@ fn js_session_options_to_rust(options: Option<SessionOptions>) -> RustSessionOpt
10691080
if o.auto_save.unwrap_or(false) {
10701081
opts = opts.with_auto_save(true);
10711082
}
1083+
if let Some(t) = o.temperature {
1084+
opts = opts.with_temperature(t as f32);
1085+
}
1086+
if let Some(budget) = o.thinking_budget {
1087+
opts = opts.with_thinking_budget(budget as usize);
1088+
}
1089+
if let Some(enabled) = o.continuation_enabled {
1090+
opts = opts.with_continuation(enabled);
1091+
}
1092+
if let Some(turns) = o.max_continuation_turns {
1093+
opts = opts.with_max_continuation_turns(turns);
1094+
}
10721095

10731096
// AHP transport configuration
10741097
#[cfg(feature = "ahp")]

0 commit comments

Comments
 (0)