Skip to content

Commit 219a361

Browse files
authored
fix(agents-core): preserve legacy ShellTool typing and strict shell pending statuses (#952)
1 parent 59fa0a8 commit 219a361

File tree

5 files changed

+124
-9
lines changed

5 files changed

+124
-9
lines changed

.changeset/few-comics-help.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openai/agents-core': patch
3+
---
4+
5+
fix: preserve ShellTool compatibility while keeping factory environment and hosted polling

packages/agents-core/src/runner/modelOutputs.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,24 @@ function resolveFunctionOrHandoff(
108108
return { type: 'function', tool: functionTool };
109109
}
110110

111+
type ShellCallStatus = 'in_progress' | 'completed' | 'incomplete';
112+
113+
function parseShellCallStatus(status: unknown): ShellCallStatus | undefined {
114+
if (
115+
status === 'in_progress' ||
116+
status === 'completed' ||
117+
status === 'incomplete'
118+
) {
119+
return status;
120+
}
121+
return undefined;
122+
}
123+
111124
function isShellCallPendingStatus(status: unknown): boolean {
112-
return status !== 'completed' && status !== 'incomplete';
125+
if (typeof status === 'undefined') {
126+
return true;
127+
}
128+
return parseShellCallStatus(status) === 'in_progress';
113129
}
114130

115131
function hasPendingShellOutputStatus(

packages/agents-core/src/tool.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -575,10 +575,6 @@ type ShellToolBase = {
575575
* Public name exposed to the model. Defaults to `shell`.
576576
*/
577577
name: string;
578-
/**
579-
* The execution environment for the shell tool.
580-
*/
581-
environment: ShellToolEnvironment;
582578
/**
583579
* Predicate determining whether this shell action requires approval.
584580
*/
@@ -591,13 +587,21 @@ type ShellToolBase = {
591587
};
592588

593589
type LocalShellTool = ShellToolBase & {
594-
environment: ShellToolLocalEnvironment;
590+
/**
591+
* Optional for backward compatibility with direct `ShellTool` literals.
592+
* When omitted, local mode is assumed.
593+
*/
594+
environment?: ShellToolLocalEnvironment;
595595
/**
596596
* The shell implementation to execute commands in local mode.
597597
*/
598598
shell: Shell;
599599
};
600600

601+
type NormalizedLocalShellTool = Omit<LocalShellTool, 'environment'> & {
602+
environment: ShellToolLocalEnvironment;
603+
};
604+
601605
type HostedShellTool = ShellToolBase & {
602606
environment: ShellToolHostedEnvironment;
603607
/**
@@ -721,11 +725,13 @@ function normalizeShellToolEnvironment(
721725
};
722726
}
723727

724-
export function shellTool(options: LocalShellToolOptions): ShellTool;
725-
export function shellTool(options: HostedShellToolOptions): ShellTool;
728+
export function shellTool(
729+
options: LocalShellToolOptions,
730+
): NormalizedLocalShellTool;
731+
export function shellTool(options: HostedShellToolOptions): HostedShellTool;
726732
export function shellTool(
727733
options: LocalShellToolOptions | HostedShellToolOptions,
728-
): ShellTool {
734+
): NormalizedLocalShellTool | HostedShellTool {
729735
const environment = normalizeShellToolEnvironment(options.environment);
730736
const needsApproval: ShellApprovalFunction =
731737
typeof options.needsApproval === 'function'

packages/agents-core/test/runner/modelOutputs.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,29 @@ describe('processModelResponse', () => {
160160
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
161161
});
162162

163+
it('keeps hosted shell calls pending when status is omitted', () => {
164+
const shellCall: protocol.ShellCallItem = {
165+
type: 'shell_call',
166+
callId: 'call_shell',
167+
action: { commands: ['echo hi'] },
168+
};
169+
const modelResponse: ModelResponse = {
170+
output: [shellCall],
171+
usage: new Usage(),
172+
};
173+
174+
const shell = shellTool({
175+
environment: { type: 'container_auto' },
176+
});
177+
const result = processModelResponse(modelResponse, TEST_AGENT, [shell], []);
178+
179+
expect(result.newItems).toHaveLength(1);
180+
expect(result.newItems[0]).toBeInstanceOf(ToolCallItem);
181+
expect(result.shellActions).toHaveLength(0);
182+
expect(result.toolsUsed).toEqual(['shell']);
183+
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
184+
});
185+
163186
it('does not keep hosted shell calls pending when incomplete', () => {
164187
const shellCall: protocol.ShellCallItem = {
165188
type: 'shell_call',
@@ -184,6 +207,30 @@ describe('processModelResponse', () => {
184207
expect(result.hasToolsOrApprovalsToRun()).toBe(false);
185208
});
186209

210+
it('does not keep hosted shell calls pending on unknown status values', () => {
211+
const shellCall: protocol.ShellCallItem = {
212+
type: 'shell_call',
213+
callId: 'call_shell',
214+
status: 'queued' as any,
215+
action: { commands: ['echo hi'] },
216+
};
217+
const modelResponse: ModelResponse = {
218+
output: [shellCall],
219+
usage: new Usage(),
220+
};
221+
222+
const shell = shellTool({
223+
environment: { type: 'container_auto' },
224+
});
225+
const result = processModelResponse(modelResponse, TEST_AGENT, [shell], []);
226+
227+
expect(result.newItems).toHaveLength(1);
228+
expect(result.newItems[0]).toBeInstanceOf(ToolCallItem);
229+
expect(result.shellActions).toHaveLength(0);
230+
expect(result.toolsUsed).toEqual(['shell']);
231+
expect(result.hasToolsOrApprovalsToRun()).toBe(false);
232+
});
233+
187234
it('preserves hosted shell output items in processed run items', () => {
188235
const shellOutput: protocol.ShellCallResultItem = {
189236
type: 'shell_call_output',
@@ -239,6 +286,35 @@ describe('processModelResponse', () => {
239286
expect(result.hasToolsOrApprovalsToRun()).toBe(true);
240287
});
241288

289+
it('does not keep hosted shell pending on unknown shell_call_output status values', () => {
290+
const shellOutput: protocol.ShellCallResultItem = {
291+
type: 'shell_call_output',
292+
callId: 'call_shell',
293+
output: [
294+
{
295+
stdout: 'partial',
296+
stderr: '',
297+
outcome: { type: 'exit', exitCode: 0 },
298+
},
299+
],
300+
providerData: {
301+
status: 'queued',
302+
},
303+
};
304+
const modelResponse: ModelResponse = {
305+
output: [shellOutput],
306+
usage: new Usage(),
307+
};
308+
309+
const result = processModelResponse(modelResponse, TEST_AGENT, [], []);
310+
311+
expect(result.newItems).toHaveLength(1);
312+
expect(result.newItems[0]).toBeInstanceOf(ToolCallOutputItem);
313+
expect(result.newItems[0].rawItem).toEqual(shellOutput);
314+
expect(result.toolsUsed).toEqual([]);
315+
expect(result.hasToolsOrApprovalsToRun()).toBe(false);
316+
});
317+
242318
it('throws when shell action emitted without shell tool', () => {
243319
const shellCall: protocol.ShellCallItem = {
244320
type: 'shell_call',

packages/agents-core/test/tool.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
resolveComputer,
99
disposeResolvedComputers,
1010
} from '../src/tool';
11+
import type { ShellTool } from '../src/tool';
1112
import { z } from 'zod';
1213
import { Computer } from '../src';
1314
import { Agent } from '../src/agent';
@@ -151,10 +152,21 @@ describe('Tool', () => {
151152
const t = shellTool({ shell });
152153
expect(t.type).toBe('shell');
153154
expect(t.name).toBe('shell');
155+
expect(t.environment.type).toBe('local');
154156
expect(t.environment).toEqual({ type: 'local' });
155157
expect(t.shell).toBe(shell);
156158
});
157159

160+
it('ShellTool keeps local environment optional for compatibility', () => {
161+
const legacyTool: ShellTool = {
162+
type: 'shell',
163+
name: 'shell',
164+
shell: new FakeShell(),
165+
needsApproval: async () => false,
166+
};
167+
expect(legacyTool.environment).toBeUndefined();
168+
});
169+
158170
it('shellTool supports hosted container environments without local shell', () => {
159171
const t = shellTool({
160172
environment: { type: 'container_reference', containerId: 'cont_123' },

0 commit comments

Comments
 (0)