feat(agent): expose profile streaming policy

This commit is contained in:
Darkatse 2026-08-28 02:35:23 -04:00
parent 8ca4b5abf6
commit 0e685aa7fa
8 changed files with 62 additions and 31 deletions

View file

@ -24,6 +24,8 @@ export default tseslint.config(
},
rules: {
'max-lines': ['error', 500],
// Switch copy is nested under label > span > strong/small.
'jsx-a11y/label-has-associated-control': ['error', { depth: 3 }],
// A focusable separator is the keyboard-operated window-splitter pattern.
'jsx-a11y/no-interactive-element-to-noninteractive-role': ['error', {
button: ['separator'],

View file

@ -138,6 +138,8 @@
"agent_system.retryIntervalMs": "重试间隔(毫秒)",
"agent_system.resizeTimelineHeight": "拖动以调节时间线高度",
"agent_system.runPolicy": "运行策略",
"agent_system.streaming": "流式传输",
"agent_system.streamingHint": "实时传输生成中的工具参数;前台 write_file 内容会同步显示在聊天中。",
"agent_system.runs": "运行记录",
"agent_system.runHistory": "运行记录",
"agent_system.runHistoryAllChats": "全部聊天",

View file

@ -138,6 +138,8 @@
"agent_system.retryIntervalMs": "重試間隔(毫秒)",
"agent_system.resizeTimelineHeight": "拖曳以調整時間線高度",
"agent_system.runPolicy": "執行策略",
"agent_system.streaming": "串流傳輸",
"agent_system.streamingHint": "即時傳輸生成中的工具參數;前台 write_file 內容會同步顯示在聊天中。",
"agent_system.runs": "執行記錄",
"agent_system.runHistory": "執行記錄",
"agent_system.runHistoryAllChats": "全部聊天",

View file

@ -66,6 +66,7 @@ export type AgentSystemDraftEditor = {
setCanDelegate: (enabled: boolean) => void;
setCanHandoff: (enabled: boolean) => void;
setRunPresentation: (presentation: string) => void;
setRunStream: (enabled: boolean) => void;
setCallableAsSubAgent: (enabled: boolean) => void;
setCallableAsHandoffTarget: (enabled: boolean) => void;
setDelegationDescription: (value: string) => void;
@ -213,6 +214,14 @@ export function createAgentSystemDraftEditor(context: AgentSystemDraftEditorCont
applyRunPresentation(draft, presentation, context.presentationMemory, memoryKey(draft));
});
},
setRunStream(enabled) {
if (isBuiltin()) {
return;
}
editDraft((draft) => {
draft.run.stream = enabled;
});
},
setCallableAsSubAgent(enabled) {
if (isBuiltin()) {
return;

View file

@ -24,27 +24,6 @@ export function ProfileMainDelegationSection({ snapshot, controller, tr }: Profi
const { draft } = snapshot;
const builtin = isBuiltinProfile(draft);
const callableHandoffTarget = isCallableAsHandoffTarget(draft);
// The label text stays in the same DOM position; hoisting it into a
// variable keeps the rendered tree identical while letting the a11y
// lint see the label copy (it only scans two levels deep).
const delegateCopy = (
<span>
<strong>{tr('delegateToSubAgents')}</strong>
<small>{tr('delegateToSubAgentsHint')}</small>
</span>
);
const handoffCopy = (
<span>
<strong>{tr('allowAgentHandoff')}</strong>
<small>{tr('allowAgentHandoffHint')}</small>
</span>
);
const handoffTargetCopy = (
<span>
<strong>{tr('callableHandoffTargetToggle')}</strong>
<small>{tr('callableHandoffTargetHint')}</small>
</span>
);
return (
<div className="ttas-section" data-ttas-profile-section="main-delegation">
<div className="ttas-section-title">
@ -59,7 +38,10 @@ export function ProfileMainDelegationSection({ snapshot, controller, tr }: Profi
disabled={builtin}
onChange={(event) => controller.setCanDelegate(event.target.checked)}
/>
{delegateCopy}
<span>
<strong>{tr('delegateToSubAgents')}</strong>
<small>{tr('delegateToSubAgentsHint')}</small>
</span>
</label>
{draft.delegation.canDelegate && (
<div className="ttas-form-grid ttas-delegation-controls">
@ -94,7 +76,10 @@ export function ProfileMainDelegationSection({ snapshot, controller, tr }: Profi
disabled={builtin}
onChange={(event) => controller.setCanHandoff(event.target.checked)}
/>
{handoffCopy}
<span>
<strong>{tr('allowAgentHandoff')}</strong>
<small>{tr('allowAgentHandoffHint')}</small>
</span>
</label>
{draft.delegation.canHandoff && (
<div className="ttas-form-grid ttas-delegation-controls">
@ -118,7 +103,10 @@ export function ProfileMainDelegationSection({ snapshot, controller, tr }: Profi
disabled={builtin}
onChange={(event) => controller.setCallableAsHandoffTarget(event.target.checked)}
/>
{handoffTargetCopy}
<span>
<strong>{tr('callableHandoffTargetToggle')}</strong>
<small>{tr('callableHandoffTargetHint')}</small>
</span>
</label>
{callableHandoffTarget && (
<div className="ttas-form-grid ttas-delegation-controls">
@ -161,12 +149,6 @@ export function ProfileSubAgentAccessSection({ snapshot, controller, tr }: Profi
const { draft } = snapshot;
const builtin = isBuiltinProfile(draft);
const callableSubAgent = isCallableAsSubAgent(draft);
const subAgentCopy = (
<span>
<strong>{tr('callableSubAgentToggle')}</strong>
<small>{tr('callableSubAgentHint')}</small>
</span>
);
return (
<div className="ttas-section" data-ttas-profile-section="subagent-access">
<div className="ttas-section-title">
@ -181,7 +163,10 @@ export function ProfileSubAgentAccessSection({ snapshot, controller, tr }: Profi
disabled={builtin}
onChange={(event) => controller.setCallableAsSubAgent(event.target.checked)}
/>
{subAgentCopy}
<span>
<strong>{tr('callableSubAgentToggle')}</strong>
<small>{tr('callableSubAgentHint')}</small>
</span>
</label>
{callableSubAgent && (
<div className="ttas-form-grid ttas-delegation-controls">
@ -231,6 +216,18 @@ export function ProfileRunSection({ snapshot, controller, tr }: ProfileSectionPr
<h4>{tr('runPolicy')}</h4>
</div>
<div className="ttas-form-grid">
<label className="ttas-switch-row ttas-span-2">
<input
type="checkbox"
checked={draft.run.stream}
disabled={builtin}
onChange={(event) => controller.setRunStream(event.target.checked)}
/>
<span>
<strong>{tr('streaming')}</strong>
<small>{tr('streamingHint')}</small>
</span>
</label>
<label className="ttas-field">
<span>{tr('presentation')}</span>
<select

View file

@ -216,6 +216,8 @@ export const DEFAULT_MESSAGES = {
"retryIntervalMs": "Retry Interval Ms",
"resizeTimelineHeight": "Drag to resize timeline height",
"runPolicy": "Run Policy",
"streaming": "Streaming",
"streamingHint": "Stream generated tool arguments. Foreground write_file content appears in chat while it is generated.",
"runs": "Runs",
"runHistory": "Run History",
"runHistoryAllChats": "All chats",

View file

@ -26,6 +26,7 @@ test('profileForEdit migrates v2 native tool names to canonical ToolIds', () =>
test('profileForEdit keeps CSV drafts separate and normalizeProfileForSave restores lists', () => {
const profile = defaultProfile('writer');
profile.run.stream = true;
profile.skills.visible = ['lore', 'tools'];
profile.delegation.allowedCallers = ['main', 'reviewer'];
@ -36,12 +37,22 @@ test('profileForEdit keeps CSV drafts separate and normalizeProfileForSave resto
draft.delegation.allowedCallersCsv = 'editor';
const saved = normalizeProfileForSave(draft);
expect(saved.run.stream).toBe(true);
expect(saved.skills.visible).toEqual(['research', 'tools']);
expect(saved.delegation.allowedCallers).toEqual(['editor']);
expect('visibleCsv' in saved.skills).toBe(false);
expect('allowedCallersCsv' in saved.delegation).toBe(false);
});
test('run streaming defaults missing fields and rejects non-booleans', () => {
const profile = defaultProfile('streaming');
Reflect.deleteProperty(profile.run, 'stream');
expect(profileForEdit(profile).run.stream).toBe(false);
Reflect.set(profile.run, 'stream', 'true');
expect(() => normalizeProfileForSave(profile)).toThrow(/run\.stream must be a boolean/);
});
test('tool description overrides preserve user text and reject invalid values', () => {
const profile = defaultProfile('descriptions');
profile.tools.toolDescriptions = {

View file

@ -207,11 +207,16 @@ function normalizeRunPolicy(value: unknown): AgentProfile['run'] {
if (presentation !== 'foreground' && presentation !== 'background') {
throw new Error(`run.presentation is unsupported: ${presentation}`);
}
const stream = policy.stream ?? false;
if (typeof stream !== 'boolean') {
throw new Error('run.stream must be a boolean');
}
const directRunnable = policy.directRunnable !== false;
const modelRetry = isPlainObject(policy.modelRetry) ? policy.modelRetry : {};
return {
presentation: directRunnable ? presentation : 'background',
stream,
directRunnable,
modelRetry: {
maxRetries: Number(modelRetry.maxRetries ?? 3),
@ -329,6 +334,7 @@ export function defaultProfile(id: string = DEFAULT_PROFILE_ID): AgentProfile {
},
run: {
presentation: 'foreground',
stream: false,
directRunnable: true,
modelRetry: {
maxRetries: 3,