mirror of
https://github.com/sst/opencode.git
synced 2025-12-23 10:11:41 +00:00
Merge branch 'dev' into agent-loop
This commit is contained in:
commit
f9fc8ed1b6
10 changed files with 146 additions and 47 deletions
1
STATS.md
1
STATS.md
|
|
@ -142,3 +142,4 @@
|
|||
| 2025-11-14 | 759,928 (+10,023) | 705,237 (+9,080) | 1,465,165 (+19,103) |
|
||||
| 2025-11-15 | 765,955 (+6,027) | 712,870 (+7,633) | 1,478,825 (+13,660) |
|
||||
| 2025-11-16 | 771,069 (+5,114) | 716,596 (+3,726) | 1,487,665 (+8,840) |
|
||||
| 2025-11-17 | 780,161 (+9,092) | 723,339 (+6,743) | 1,503,500 (+15,835) |
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export namespace Agent {
|
|||
.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]),
|
||||
mode: z.enum(["subagent", "primary", "all"]),
|
||||
builtIn: z.boolean(),
|
||||
topP: z.number().optional(),
|
||||
temperature: z.number().optional(),
|
||||
|
|
|
|||
|
|
@ -665,7 +665,11 @@ export function Prompt(props: PromptProps) {
|
|||
return
|
||||
}
|
||||
|
||||
const pastedContent = event.text.trim()
|
||||
// Normalize line endings at the boundary
|
||||
// Windows ConPTY/Terminal often sends CR-only newlines in bracketed paste
|
||||
// Replace CRLF first, then any remaining CR
|
||||
const normalizedText = event.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")
|
||||
const pastedContent = normalizedText.trim()
|
||||
if (!pastedContent) {
|
||||
command.trigger("prompt.paste")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export namespace Config {
|
|||
for (const dir of directories) {
|
||||
await assertValid(dir)
|
||||
|
||||
if (dir.endsWith(".opencode")) {
|
||||
if (dir.endsWith(".opencode") || dir === Flag.OPENCODE_CONFIG_DIR) {
|
||||
for (const file of ["opencode.jsonc", "opencode.json"]) {
|
||||
log.debug(`loading config from ${path.join(dir, file)}`)
|
||||
result = mergeDeep(result, await loadFile(path.join(dir, file)))
|
||||
|
|
@ -337,7 +337,7 @@ export namespace Config {
|
|||
export const Mcp = z.discriminatedUnion("type", [McpLocal, McpRemote])
|
||||
export type Mcp = z.infer<typeof Mcp>
|
||||
|
||||
export const Permission = z.union([z.literal("ask"), z.literal("allow"), z.literal("deny")])
|
||||
export const Permission = z.enum(["ask", "allow", "deny"])
|
||||
export type Permission = z.infer<typeof Permission>
|
||||
|
||||
export const Command = z.object({
|
||||
|
|
@ -358,7 +358,7 @@ export namespace Config {
|
|||
tools: z.record(z.string(), z.boolean()).optional(),
|
||||
disable: z.boolean().optional(),
|
||||
description: z.string().optional().describe("Description of when to use the agent"),
|
||||
mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(),
|
||||
mode: z.enum(["subagent", "primary", "all"]).optional(),
|
||||
color: z
|
||||
.string()
|
||||
.regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format")
|
||||
|
|
|
|||
|
|
@ -78,6 +78,22 @@ export namespace Provider {
|
|||
options: {},
|
||||
}
|
||||
},
|
||||
"azure-cognitive-services": async () => {
|
||||
const resourceName = process.env["AZURE_COGNITIVE_SERVICES_RESOURCE_NAME"]
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string, options?: Record<string, any>) {
|
||||
if (options?.["useCompletionUrls"]) {
|
||||
return sdk.chat(modelID)
|
||||
} else {
|
||||
return sdk.responses(modelID)
|
||||
}
|
||||
},
|
||||
options: {
|
||||
baseURL: resourceName ? `https://${resourceName}.cognitiveservices.azure.com/openai` : undefined,
|
||||
},
|
||||
}
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"] && !process.env["AWS_BEARER_TOKEN_BEDROCK"])
|
||||
return { autoload: false }
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const ERRORS = {
|
|||
schema: resolver(
|
||||
z
|
||||
.object({
|
||||
data: z.any().nullable(),
|
||||
data: z.any(),
|
||||
errors: z.array(z.record(z.string(), z.any())),
|
||||
success: z.literal(false),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -89,10 +89,18 @@ export const BashTool = Tool.define("bash", {
|
|||
.text()
|
||||
.then((x) => x.trim())
|
||||
log.info("resolved path", { arg, resolved })
|
||||
if (resolved && !Filesystem.contains(Instance.directory, resolved)) {
|
||||
throw new Error(
|
||||
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
|
||||
)
|
||||
if (resolved) {
|
||||
// Git Bash on Windows returns Unix-style paths like /c/Users/...
|
||||
const normalized =
|
||||
process.platform === "win32" && resolved.match(/^\/[a-z]\//)
|
||||
? resolved.replace(/^\/([a-z])\//, (_, drive) => `${drive.toUpperCase()}:\\`).replace(/\//g, "\\")
|
||||
: resolved
|
||||
|
||||
if (!Filesystem.contains(Instance.directory, normalized)) {
|
||||
throw new Error(
|
||||
`This command references paths outside of ${Instance.directory} so it is not allowed to be executed.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export const BatchTool = Tool.define("batch", async () => {
|
|||
}),
|
||||
)
|
||||
.min(1, "Provide at least one tool call")
|
||||
.max(10, "Too many tools in batch. Maximum allowed is 10.")
|
||||
.describe("Array of tool calls to execute in parallel"),
|
||||
}),
|
||||
formatValidationError(error) {
|
||||
|
|
@ -34,34 +33,16 @@ export const BatchTool = Tool.define("batch", async () => {
|
|||
const { Session } = await import("../session")
|
||||
const { Identifier } = await import("../id/id")
|
||||
|
||||
const toolCalls = params.tool_calls
|
||||
const toolCalls = params.tool_calls.slice(0, 10)
|
||||
const discardedCalls = params.tool_calls.slice(10)
|
||||
|
||||
const { ToolRegistry } = await import("./registry")
|
||||
const availableTools = await ToolRegistry.tools("", "")
|
||||
const toolMap = new Map(availableTools.map((t) => [t.id, t]))
|
||||
|
||||
const partIDs = new Map<(typeof toolCalls)[0], string>()
|
||||
for (const call of toolCalls) {
|
||||
const partID = Identifier.ascending("part")
|
||||
partIDs.set(call, partID)
|
||||
Session.updatePart({
|
||||
id: partID,
|
||||
messageID: ctx.messageID,
|
||||
sessionID: ctx.sessionID,
|
||||
type: "tool",
|
||||
tool: call.tool,
|
||||
callID: partID,
|
||||
state: {
|
||||
status: "pending",
|
||||
input: call.parameters,
|
||||
raw: JSON.stringify(call),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const executeCall = async (call: (typeof toolCalls)[0]) => {
|
||||
const callStartTime = Date.now()
|
||||
const partID = partIDs.get(call)!
|
||||
const partID = Identifier.ascending("part")
|
||||
|
||||
try {
|
||||
if (DISALLOWED.has(call.tool)) {
|
||||
|
|
@ -77,6 +58,22 @@ export const BatchTool = Tool.define("batch", async () => {
|
|||
}
|
||||
const validatedParams = tool.parameters.parse(call.parameters)
|
||||
|
||||
await Session.updatePart({
|
||||
id: partID,
|
||||
messageID: ctx.messageID,
|
||||
sessionID: ctx.sessionID,
|
||||
type: "tool",
|
||||
tool: call.tool,
|
||||
callID: partID,
|
||||
state: {
|
||||
status: "running",
|
||||
input: call.parameters,
|
||||
time: {
|
||||
start: callStartTime,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const result = await tool.execute(validatedParams, { ...ctx, callID: partID })
|
||||
|
||||
await Session.updatePart({
|
||||
|
|
@ -126,31 +123,48 @@ export const BatchTool = Tool.define("batch", async () => {
|
|||
|
||||
const results = await Promise.all(toolCalls.map((call) => executeCall(call)))
|
||||
|
||||
const successfulCalls = results.filter((r) => r.success).length
|
||||
const failedCalls = toolCalls.length - successfulCalls
|
||||
// Add discarded calls as errors
|
||||
const now = Date.now()
|
||||
for (const call of discardedCalls) {
|
||||
const partID = Identifier.ascending("part")
|
||||
await Session.updatePart({
|
||||
id: partID,
|
||||
messageID: ctx.messageID,
|
||||
sessionID: ctx.sessionID,
|
||||
type: "tool",
|
||||
tool: call.tool,
|
||||
callID: partID,
|
||||
state: {
|
||||
status: "error",
|
||||
input: call.parameters,
|
||||
error: "Maximum of 10 tools allowed in batch",
|
||||
time: { start: now, end: now },
|
||||
},
|
||||
})
|
||||
results.push({
|
||||
success: false as const,
|
||||
tool: call.tool,
|
||||
error: new Error("Maximum of 10 tools allowed in batch"),
|
||||
})
|
||||
}
|
||||
|
||||
const outputParts = results.map((r) => {
|
||||
if (r.success) {
|
||||
return `<tool_result name="${r.tool}">\n${r.result.output}\n</tool_result>`
|
||||
}
|
||||
const errorMessage = r.error instanceof Error ? r.error.message : String(r.error)
|
||||
return `<tool_result name="${r.tool}">\nError: ${errorMessage}\n</tool_result>`
|
||||
})
|
||||
const successfulCalls = results.filter((r) => r.success).length
|
||||
const failedCalls = results.length - successfulCalls
|
||||
|
||||
const outputMessage =
|
||||
failedCalls > 0
|
||||
? `Executed ${successfulCalls}/${toolCalls.length} tools successfully. ${failedCalls} failed.\n\n${outputParts.join("\n\n")}`
|
||||
: `All ${successfulCalls} tools executed successfully.\n\n${outputParts.join("\n\n")}\n\nKeep using the batch tool for optimal performance in your next response!`
|
||||
? `Executed ${successfulCalls}/${results.length} tools successfully. ${failedCalls} failed.`
|
||||
: `All ${successfulCalls} tools executed successfully.\n\nKeep using the batch tool for optimal performance in your next response!`
|
||||
|
||||
return {
|
||||
title: `Batch execution (${successfulCalls}/${toolCalls.length} successful)`,
|
||||
title: `Batch execution (${successfulCalls}/${results.length} successful)`,
|
||||
output: outputMessage,
|
||||
attachments: results.filter((result) => result.success).flatMap((r) => r.result.attachments ?? []),
|
||||
metadata: {
|
||||
totalCalls: toolCalls.length,
|
||||
totalCalls: results.length,
|
||||
successful: successfulCalls,
|
||||
failed: failedCalls,
|
||||
tools: toolCalls.map((c) => c.tool),
|
||||
tools: params.tool_calls.map((c) => c.tool),
|
||||
details: results.map((r) => ({ tool: r.tool, success: r.success })),
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1183,7 +1183,7 @@ export type Config = {
|
|||
}
|
||||
|
||||
export type BadRequestError = {
|
||||
data: unknown | null
|
||||
data: unknown
|
||||
errors: Array<{
|
||||
[key: string]: unknown
|
||||
}>
|
||||
|
|
|
|||
|
|
@ -229,6 +229,62 @@ Or if you already have an API key, you can select **Manually enter API Key** and
|
|||
|
||||
---
|
||||
|
||||
### Azure Cognitive Services
|
||||
|
||||
1. Head over to the [Azure portal](https://portal.azure.com/) and create an **Azure OpenAI** resource. You'll need:
|
||||
- **Resource name**: This becomes part of your API endpoint (`https://AZURE_COGNITIVE_SERVICES_RESOURCE_NAME.cognitiveservices.azure.com/`)
|
||||
- **API key**: Either `KEY 1` or `KEY 2` from your resource
|
||||
|
||||
2. Go to [Azure AI Foundry](https://ai.azure.com/) and deploy a model.
|
||||
|
||||
:::note
|
||||
The deployment name must match the model name for opencode to work properly.
|
||||
:::
|
||||
|
||||
3. Run `opencode auth login` and select **Azure**.
|
||||
|
||||
```bash
|
||||
$ opencode auth login
|
||||
|
||||
┌ Add credential
|
||||
│
|
||||
◆ Select provider
|
||||
│ ● Azure Cognitive Services
|
||||
│ ...
|
||||
└
|
||||
```
|
||||
|
||||
4. Enter your API key.
|
||||
|
||||
```bash
|
||||
$ opencode auth login
|
||||
|
||||
┌ Add credential
|
||||
│
|
||||
◇ Select provider
|
||||
│ Azure Cognitive Services
|
||||
│
|
||||
◇ Enter your API key
|
||||
│ _
|
||||
└
|
||||
```
|
||||
|
||||
5. Set your resource name as an environment variable:
|
||||
|
||||
```bash
|
||||
AZURE_COGNITIVE_SERVICES_RESOURCE_NAME=XXX opencode
|
||||
```
|
||||
|
||||
Or add it to your bash profile:
|
||||
|
||||
```bash title="~/.bash_profile"
|
||||
export AZURE_COGNITIVE_SERVICES_RESOURCE_NAME=XXX
|
||||
```
|
||||
|
||||
6. Run the `/models` command to select your deployed model.
|
||||
|
||||
---
|
||||
|
||||
### Baseten
|
||||
|
||||
1. Head over to the [Baseten](https://app.baseten.co/), create an account, and generate an API key.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue