Skip to content

Commit 3e57a7c

Browse files
committed
making it scout
1 parent 73ee22f commit 3e57a7c

File tree

15 files changed

+665
-171
lines changed

15 files changed

+665
-171
lines changed

bun.lock

Lines changed: 524 additions & 35 deletions
Large diffs are not rendered by default.

packages/scout-agent/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.blink

packages/scout-agent/agent.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { tool } from "ai";
22
import * as blink from "blink";
33
import { z } from "zod";
4-
import { GeneralPurposeCore, type Message, type Options } from "./lib";
4+
import { type Message, type Options, Scout } from "./lib";
55

66
export const agent = new blink.Agent<blink.WithUIOptions<Options, Message>>();
77

8-
const core = new GeneralPurposeCore({
8+
const scout = new Scout({
99
agent,
1010
github: {
1111
appID: process.env.GITHUB_APP_ID,
@@ -27,16 +27,16 @@ const core = new GeneralPurposeCore({
2727
agent.on("request", async (request) => {
2828
const url = new URL(request.url);
2929
if (url.pathname.startsWith("/slack")) {
30-
return core.handleSlackWebhook(request);
30+
return scout.handleSlackWebhook(request);
3131
}
3232
if (url.pathname.startsWith("/github")) {
33-
return core.handleGitHubWebhook(request);
33+
return scout.handleGitHubWebhook(request);
3434
}
3535
return new Response("Hey there!", { status: 200 });
3636
});
3737

3838
agent.on("chat", async ({ id, messages }) => {
39-
return core.streamStepResponse({
39+
return scout.streamStepResponse({
4040
chatID: id,
4141
messages,
4242
model: "anthropic/claude-sonnet-4.5",
@@ -45,12 +45,8 @@ agent.on("chat", async ({ id, messages }) => {
4545
get_favorite_color: tool({
4646
description: "Get your favorite color",
4747
inputSchema: z.object({}),
48-
async *execute() {
49-
yield "blue";
50-
await new Promise((resolve) => setTimeout(resolve, 1000));
51-
yield "red";
52-
await new Promise((resolve) => setTimeout(resolve, 1000));
53-
yield "green";
48+
execute() {
49+
return "blue";
5450
},
5551
}),
5652
},

packages/scout-agent/biome.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
"ignoreUnknown": false
1010
},
1111
"formatter": {
12-
"enabled": true,
13-
"indentStyle": "space"
12+
"enabled": false
1413
},
1514
"linter": {
1615
"enabled": true,

packages/scout-agent/lib/compute/common.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { WebSocket } from "ws";
55

66
export const WORKSPACE_INFO_KEY = "__compute_workspace_id";
77

8-
export const newComputeClient = async (ws: WebSocket) => {
8+
export const newComputeClient = async (ws: WebSocket): Promise<Client> => {
99
return new Promise<Client>((resolve, reject) => {
1010
const encoder = new TextEncoder();
1111
const decoder = new TextDecoder();

packages/scout-agent/lib/compute/docker.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const parseExecOutput = (output: unknown): string => {
2020
};
2121

2222
const execProcess = async (
23-
command: string,
23+
command: string
2424
): Promise<{ stdout: string; stderr: string; exitCode: number }> => {
2525
try {
2626
const output = await exec(command, {});
@@ -42,7 +42,9 @@ const execProcess = async (
4242
}
4343
};
4444

45-
const dockerWorkspaceInfoSchema = z.object({
45+
const dockerWorkspaceInfoSchema: z.ZodObject<{
46+
containerName: z.ZodString;
47+
}> = z.object({
4648
containerName: z.string(),
4749
});
4850

@@ -86,13 +88,13 @@ export const initializeDockerWorkspace =
8688
const { exitCode: versionExitCode } = await execProcess("docker --version");
8789
if (versionExitCode !== 0) {
8890
throw new Error(
89-
`Docker is not available. Please install it or choose a different workspace provider.`,
91+
`Docker is not available. Please install it or choose a different workspace provider.`
9092
);
9193
}
9294

9395
const imageName = `blink-workspace:${DOCKERFILE_HASH}`;
9496
const { exitCode: dockerImageExistsExitCode } = await execProcess(
95-
`docker image inspect ${imageName}`,
97+
`docker image inspect ${imageName}`
9698
);
9799
if (dockerImageExistsExitCode !== 0) {
98100
const buildCmd = `echo "${DOCKERFILE_BASE64}" | base64 -d | docker build -t ${imageName} -f - .`;
@@ -103,14 +105,14 @@ export const initializeDockerWorkspace =
103105
} = await execProcess(buildCmd);
104106
if (buildExitCode !== 0) {
105107
throw new Error(
106-
`Failed to build docker image ${imageName}. Build output: ${buildStdout}\n${buildStderr}`,
108+
`Failed to build docker image ${imageName}. Build output: ${buildStdout}\n${buildStderr}`
107109
);
108110
}
109111
}
110112

111113
const containerName = `blink-workspace-${crypto.randomUUID()}`;
112114
const { exitCode: runExitCode } = await execProcess(
113-
`docker run -d --publish ${COMPUTE_SERVER_PORT} --name ${containerName} ${imageName} bash -c 'echo "${BOOTSTRAP_SCRIPT_BASE64}" | base64 -d | bash'`,
115+
`docker run -d --publish ${COMPUTE_SERVER_PORT} --name ${containerName} ${imageName} bash -c 'echo "${BOOTSTRAP_SCRIPT_BASE64}" | base64 -d | bash'`
114116
);
115117
if (runExitCode !== 0) {
116118
throw new Error(`Failed to run docker container ${containerName}`);
@@ -124,11 +126,11 @@ export const initializeDockerWorkspace =
124126
stdout,
125127
stderr,
126128
} = await execProcess(
127-
`docker container inspect -f json ${containerName}`,
129+
`docker container inspect -f json ${containerName}`
128130
);
129131
if (inspectExitCode !== 0) {
130132
throw new Error(
131-
`Failed to run docker container ${containerName}. Inspect failed: ${stdout}\n${stderr}`,
133+
`Failed to run docker container ${containerName}. Inspect failed: ${stdout}\n${stderr}`
132134
);
133135
}
134136
const inspectOutput = dockerInspectSchema.parse(JSON.parse(stdout));
@@ -137,7 +139,7 @@ export const initializeDockerWorkspace =
137139
}
138140
if (Date.now() - start > timeout) {
139141
throw new Error(
140-
`Timeout waiting for docker container ${containerName} to start.`,
142+
`Timeout waiting for docker container ${containerName} to start.`
141143
);
142144
}
143145
const {
@@ -147,7 +149,7 @@ export const initializeDockerWorkspace =
147149
} = await execProcess(`docker container logs ${containerName}`);
148150
if (logsExitCode !== 0) {
149151
throw new Error(
150-
`Failed to get logs for docker container ${containerName}. Logs: ${logsOutput}\n${logsStderr}`,
152+
`Failed to get logs for docker container ${containerName}. Logs: ${logsOutput}\n${logsStderr}`
151153
);
152154
}
153155
if (logsOutput.includes("Compute server running")) {
@@ -166,15 +168,15 @@ const dockerInspectSchema = z.array(
166168
IPAddress: z.string(),
167169
Ports: z.object({
168170
[`${COMPUTE_SERVER_PORT}/tcp`]: z.array(
169-
z.object({ HostPort: z.string() }),
171+
z.object({ HostPort: z.string() })
170172
),
171173
}),
172174
}),
173-
}),
175+
})
174176
);
175177

176178
export const getDockerWorkspaceClient = async (
177-
workspaceInfoRaw: unknown,
179+
workspaceInfoRaw: unknown
178180
): Promise<Client> => {
179181
const {
180182
data: workspaceInfo,
@@ -187,33 +189,33 @@ export const getDockerWorkspaceClient = async (
187189

188190
const { stdout: dockerInspectRawOutput, exitCode: inspectExitCode } =
189191
await execProcess(
190-
`docker container inspect -f json ${workspaceInfo.containerName}`,
192+
`docker container inspect -f json ${workspaceInfo.containerName}`
191193
);
192194
if (inspectExitCode !== 0) {
193195
throw new Error(
194-
`Failed to inspect docker container ${workspaceInfo.containerName}. Initialize a new workspace with initialize_workspace first.`,
196+
`Failed to inspect docker container ${workspaceInfo.containerName}. Initialize a new workspace with initialize_workspace first.`
195197
);
196198
}
197199
const dockerInspect = dockerInspectSchema.parse(
198-
JSON.parse(dockerInspectRawOutput),
200+
JSON.parse(dockerInspectRawOutput)
199201
);
200202
const ipAddress = dockerInspect[0]?.NetworkSettings.IPAddress;
201203
if (!ipAddress) {
202204
throw new Error(
203-
`Could not find IP address for docker container ${workspaceInfo.containerName}`,
205+
`Could not find IP address for docker container ${workspaceInfo.containerName}`
204206
);
205207
}
206208
if (!dockerInspect[0]?.State.Running) {
207209
throw new Error(
208-
`Docker container ${workspaceInfo.containerName} is not running.`,
210+
`Docker container ${workspaceInfo.containerName} is not running.`
209211
);
210212
}
211213
const hostPort =
212214
dockerInspect[0]?.NetworkSettings.Ports[`${COMPUTE_SERVER_PORT}/tcp`]?.[0]
213215
?.HostPort;
214216
if (!hostPort) {
215217
throw new Error(
216-
`Could not find host port for docker container ${workspaceInfo.containerName}`,
218+
`Could not find host port for docker container ${workspaceInfo.containerName}`
217219
);
218220
}
219221
return newComputeClient(new WebSocket(`ws://localhost:${hostPort}`));

packages/scout-agent/lib/compute/tools.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as compute from "@blink-sdk/compute";
22
import type { Client } from "@blink-sdk/compute-protocol/client";
33
import * as github from "@blink-sdk/github";
4-
import { tool } from "ai";
4+
import { type Tool, tool } from "ai";
55
import * as blink from "blink";
66
import { z } from "zod";
77
import { getGithubAppContext } from "../github";
@@ -10,25 +10,23 @@ import { WORKSPACE_INFO_KEY } from "./common";
1010

1111
export const createComputeTools = ({
1212
agent,
13-
messages,
1413
githubConfig,
1514
initializeWorkspace,
1615
createWorkspaceClient,
1716
}: {
1817
agent: blink.Agent<Message>;
19-
messages: Message[];
2018
initializeWorkspace: () => Promise<unknown>;
2119
createWorkspaceClient: (workspaceInfo: unknown) => Promise<Client>;
2220
githubConfig?: {
2321
appID: string;
2422
privateKey: string;
2523
};
26-
}) => {
24+
}): Record<string, Tool> => {
2725
const newClient = async () => {
2826
const workspaceInfo = await agent.store.get(WORKSPACE_INFO_KEY);
2927
if (!workspaceInfo) {
3028
throw new Error(
31-
"Workspace not initialized. Call initialize_workspace first.",
29+
"Workspace not initialized. Call initialize_workspace first."
3230
);
3331
}
3432
const parsedWorkspaceInfo = JSON.parse(workspaceInfo);
@@ -43,7 +41,7 @@ export const createComputeTools = ({
4341
const workspaceInfo = await initializeWorkspace();
4442
await agent.store.set(
4543
WORKSPACE_INFO_KEY,
46-
JSON.stringify(workspaceInfo),
44+
JSON.stringify(workspaceInfo)
4745
);
4846
return "Workspace initialized.";
4947
},
@@ -71,11 +69,10 @@ It's safe to call this multiple times - re-authenticating is perfectly fine and
7169
const githubAppContext = await getGithubAppContext({
7270
githubAppID: githubConfig.appID,
7371
githubAppPrivateKey: githubConfig.privateKey,
74-
messages,
7572
});
7673
if (!githubAppContext) {
7774
throw new Error(
78-
"You can only use public repositories in this context.",
75+
"You can only use public repositories in this context."
7976
);
8077
}
8178
const token = await github.authenticateApp({
@@ -98,7 +95,7 @@ It's safe to call this multiple times - re-authenticating is perfectly fine and
9895
});
9996
if (respWait.exit_code !== 0) {
10097
throw new Error(
101-
`Failed to authenticate with Git. Output: ${respWait.plain_output.lines.join("\n")}`,
98+
`Failed to authenticate with Git. Output: ${respWait.plain_output.lines.join("\n")}`
10299
);
103100
}
104101
return "Git authenticated.";

packages/scout-agent/lib/core.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { MockLanguageModelV2 } from "ai/test";
88
import * as blink from "blink";
99
import { Client } from "blink/client";
10-
import { GeneralPurposeCore, type Message, type Options } from "./index";
10+
import { type Message, type Options, Scout } from "./index";
1111

1212
// Add async iterator support to ReadableStream for testing
1313
declare global {
@@ -50,10 +50,10 @@ const newMockModel = ({
5050

5151
const newAgent = (options: {
5252
model: MockLanguageModelV2;
53-
core?: Omit<ConstructorParameters<typeof GeneralPurposeCore>[0], "agent">;
53+
core?: Omit<ConstructorParameters<typeof Scout>[0], "agent">;
5454
}) => {
5555
const agent = new blink.Agent<blink.WithUIOptions<Options, Message>>();
56-
const core = new GeneralPurposeCore({ agent, ...options.core });
56+
const core = new Scout({ agent, ...options.core });
5757
agent.on("request", async () => {
5858
return new Response("Hello, world!", { status: 200 });
5959
});
@@ -166,7 +166,7 @@ const newPromise = <T>(timeoutMs: number = 5000) => {
166166

167167
test("core class name", () => {
168168
// biome-ignore lint/complexity/useLiteralKeys: accessing a private field
169-
expect(GeneralPurposeCore["CLASS_NAME"]).toBe(GeneralPurposeCore.name);
169+
expect(Scout["CLASS_NAME"]).toBe(Scout.name);
170170
});
171171

172172
describe("config", async () => {
@@ -196,7 +196,7 @@ describe("config", async () => {
196196
"Did you provide all required environment variables?"
197197
);
198198
expect(log).toInclude(
199-
`Alternatively, you can suppress this message by setting \`suppressConfigWarnings\` to \`true\` on \`${GeneralPurposeCore.name}\`.`
199+
`Alternatively, you can suppress this message by setting \`suppressConfigWarnings\` to \`true\` on \`${Scout.name}\`.`
200200
);
201201
},
202202
},

0 commit comments

Comments
 (0)