Skip to content

Commit fcc20bb

Browse files
committed
feat(coder): turn 1/2 for #1160
1 parent fbeadb3 commit fcc20bb

3 files changed

Lines changed: 79 additions & 5 deletions

File tree

src/tools/runs/get_actor_run_log.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { HELPER_TOOLS } from '../../const.js';
44
import type { InternalToolArgs, ToolEntry, ToolInputSchema } from '../../types.js';
55
import { TOOL_TYPE } from '../../types.js';
66
import { compileSchema } from '../../utils/ajv.js';
7-
import { respondRaw } from '../../utils/mcp.js';
7+
import { respondOk } from '../../utils/mcp.js';
8+
import { getActorRunLogToolOutputSchema } from '../structured_output_schemas.js';
89

910
const GetRunLogArgs = z.object({
1011
runId: z.string().describe('The ID of the Actor run.'),
@@ -29,9 +30,7 @@ USAGE EXAMPLES:
2930
- user_input: Show last 20 lines of logs for run y2h7sK3Wc
3031
- user_input: Get logs for run y2h7sK3Wc`,
3132
inputSchema: z.toJSONSchema(GetRunLogArgs) as ToolInputSchema,
32-
// It does not make sense to add structured output here since the log API just returns plain text
33-
// TODO(#1160): no `outputSchema`, so the `tools/call` result projection against an advertised
34-
// schema does not apply to this tool either way.
33+
outputSchema: getActorRunLogToolOutputSchema,
3534
ajvValidate: compileSchema(z.toJSONSchema(GetRunLogArgs)),
3635
paymentRequired: true,
3736
annotations: {
@@ -47,6 +46,6 @@ USAGE EXAMPLES:
4746
const v = (await client.run(parsed.runId).log().get()) ?? '';
4847
const lines = v.split('\n');
4948
const text = lines.slice(lines.length - parsed.lines - 1, lines.length).join('\n');
50-
return respondRaw({ content: [{ type: 'text', text }] });
49+
return respondOk(text, { structuredContent: { log: text } });
5150
},
5251
} as const);

src/tools/structured_output_schemas.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,15 @@ const actorRunListItemSchema = {
549549
/** Schema for get-actor-run-list output (paginated list of runs). */
550550
export const actorRunListOutputSchema = paginatedListOutputSchema(actorRunListItemSchema, 'Actor runs.');
551551

552+
/** Schema for get-actor-run-log output. Plain-text log wrapped for uniformity (#1160). */
553+
export const getActorRunLogToolOutputSchema = {
554+
type: 'object' as const,
555+
properties: {
556+
log: { type: 'string', description: 'Last N lines of the run log (plain text).' },
557+
},
558+
required: ['log'],
559+
};
560+
552561
/**
553562
* Schema for dataset items retrieval tools (get-dataset-items).
554563
* Contains dataset items with pagination and count information.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
import { HELPER_TOOLS } from '../../src/const.js';
4+
import { getActorRunLog } from '../../src/tools/runs/get_actor_run_log.js';
5+
import { getActorRunLogToolOutputSchema } from '../../src/tools/structured_output_schemas.js';
6+
import type { HelperTool, InternalToolArgs } from '../../src/types.js';
7+
import {
8+
expectSchemaConformingStructuredContent,
9+
stubToolCallContext,
10+
type TextToolResult,
11+
} from './helpers/tool_context.js';
12+
13+
const getMock = vi.fn();
14+
const logMock = vi.fn(() => ({ get: getMock }));
15+
const runMock = vi.fn(() => ({ log: logMock }));
16+
17+
const stubClient = { run: runMock } as unknown as InternalToolArgs['apifyClient'];
18+
19+
const LOG_LINES = Array.from({ length: 20 }, (_, i) => `line ${i + 1}`);
20+
const LOG_TEXT = LOG_LINES.join('\n');
21+
22+
describe('get-actor-run-log', () => {
23+
it('has the expected tool name', () => {
24+
expect(getActorRunLog.name).toBe(HELPER_TOOLS.ACTOR_RUNS_LOG);
25+
});
26+
27+
it('declares an outputSchema', () => {
28+
expect((getActorRunLog as HelperTool).outputSchema).toMatchObject({ type: 'object' });
29+
});
30+
31+
it('returns the last N lines matching the tool slicing', async () => {
32+
getMock.mockResolvedValue(LOG_TEXT);
33+
34+
const result = await (getActorRunLog as HelperTool).call(
35+
stubToolCallContext({ runId: 'run-1', lines: 5 }, stubClient),
36+
);
37+
const { content } = result as TextToolResult;
38+
39+
// Expected derived from the same slice expression the tool uses (current behavior; the -1
40+
// off-by-one is intentional and out of scope here).
41+
const expected = LOG_LINES.slice(LOG_LINES.length - 5 - 1, LOG_LINES.length).join('\n');
42+
expect(content[0].text).toBe(expected);
43+
expect(runMock).toHaveBeenCalledWith('run-1');
44+
});
45+
46+
it('mirrors the text content in structuredContent.log', async () => {
47+
getMock.mockResolvedValue(LOG_TEXT);
48+
49+
const result = await (getActorRunLog as HelperTool).call(
50+
stubToolCallContext({ runId: 'run-1', lines: 5 }, stubClient),
51+
);
52+
const { content, structuredContent } = result as TextToolResult;
53+
54+
expect((structuredContent as { log: string }).log).toBe(content[0].text);
55+
});
56+
57+
it('emits structuredContent conforming to the outputSchema', async () => {
58+
getMock.mockResolvedValue(LOG_TEXT);
59+
60+
const result = await (getActorRunLog as HelperTool).call(
61+
stubToolCallContext({ runId: 'run-1', lines: 5 }, stubClient),
62+
);
63+
64+
expectSchemaConformingStructuredContent(result, getActorRunLogToolOutputSchema);
65+
});
66+
});

0 commit comments

Comments
 (0)