chore: merge dev into v2
This commit is contained in:
commit
6cd5812ca1
304 changed files with 24678 additions and 4507 deletions
|
|
@ -59,13 +59,13 @@ export type AskResult = typeof AskResult.Type
|
|||
|
||||
export const Event = Permission.Event
|
||||
|
||||
export class RejectedError extends Schema.TaggedErrorClass<RejectedError>()("PermissionV2.RejectedError", {}) {}
|
||||
export class DeclinedError extends Schema.TaggedErrorClass<DeclinedError>()("PermissionV2.DeclinedError", {}) {}
|
||||
|
||||
export class CorrectedError extends Schema.TaggedErrorClass<CorrectedError>()("PermissionV2.CorrectedError", {
|
||||
feedback: Schema.String,
|
||||
}) {}
|
||||
|
||||
export class DeniedError extends Schema.TaggedErrorClass<DeniedError>()("PermissionV2.DeniedError", {
|
||||
export class BlockedError extends Schema.TaggedErrorClass<BlockedError>()("PermissionV2.BlockedError", {
|
||||
rules: Permission.Ruleset,
|
||||
}) {}
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ export class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("Per
|
|||
requestID: ID,
|
||||
}) {}
|
||||
|
||||
export type Error = DeniedError | RejectedError | CorrectedError
|
||||
export type Error = BlockedError | CorrectedError
|
||||
|
||||
export function evaluate(action: string, resource: string, ...rulesets: Permission.Ruleset[]): Permission.Rule {
|
||||
return (
|
||||
|
|
@ -105,7 +105,7 @@ export class Service extends Context.Service<Service, Interface>()("@opencode/v2
|
|||
interface Pending {
|
||||
readonly request: Request
|
||||
readonly agent?: AgentV2.ID
|
||||
readonly deferred: Deferred.Deferred<void, RejectedError | CorrectedError>
|
||||
readonly deferred: Deferred.Deferred<void, DeclinedError | CorrectedError>
|
||||
}
|
||||
|
||||
const layer = Layer.effect(
|
||||
|
|
@ -119,7 +119,7 @@ const layer = Layer.effect(
|
|||
const pending = new Map<ID, Pending>()
|
||||
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new RejectedError()), {
|
||||
Effect.forEach(pending.values(), (item) => Deferred.fail(item.deferred, new DeclinedError()), {
|
||||
discard: true,
|
||||
}).pipe(
|
||||
Effect.ensuring(
|
||||
|
|
@ -175,7 +175,7 @@ const layer = Layer.effect(
|
|||
const create = (request: Request, agent?: AgentV2.ID) =>
|
||||
Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
|
||||
const deferred = yield* Deferred.make<void, DeclinedError | CorrectedError>()
|
||||
const item = { request, agent, deferred }
|
||||
if (pending.has(request.id))
|
||||
return yield* Effect.die(new Error(`Duplicate pending permission ID: ${request.id}`))
|
||||
|
|
@ -199,13 +199,14 @@ const layer = Layer.effect(
|
|||
Effect.gen(function* () {
|
||||
const result = yield* evaluateInput(input)
|
||||
if (result.effect === "deny") {
|
||||
return yield* new DeniedError({
|
||||
return yield* new BlockedError({
|
||||
rules: relevant(input, result.rules),
|
||||
})
|
||||
}
|
||||
if (result.effect === "allow") return
|
||||
const item = yield* create(request(input), input.agent)
|
||||
return yield* restore(Deferred.await(item.deferred)).pipe(
|
||||
Effect.catchTag("PermissionV2.DeclinedError", (error) => Effect.die(error)),
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
pending.delete(item.request.id)
|
||||
|
|
@ -230,7 +231,7 @@ const layer = Layer.effect(
|
|||
if (input.reply === "reject") {
|
||||
yield* Deferred.fail(
|
||||
existing.deferred,
|
||||
input.message ? new CorrectedError({ feedback: input.message }) : new RejectedError(),
|
||||
input.message ? new CorrectedError({ feedback: input.message }) : new DeclinedError(),
|
||||
)
|
||||
pending.delete(input.requestID)
|
||||
for (const [id, item] of pending) {
|
||||
|
|
@ -240,7 +241,7 @@ const layer = Layer.effect(
|
|||
requestID: item.request.id,
|
||||
reply: "reject",
|
||||
})
|
||||
yield* Deferred.fail(item.deferred, new RejectedError())
|
||||
yield* Deferred.fail(item.deferred, new DeclinedError())
|
||||
pending.delete(id)
|
||||
}
|
||||
return
|
||||
|
|
|
|||
|
|
@ -3,14 +3,6 @@ import { ModelV2 } from "../../model"
|
|||
import { define } from "@opencode-ai/plugin/v2/effect/plugin"
|
||||
import { ProviderV2 } from "../../provider"
|
||||
|
||||
function shouldUseResponses(modelID: string) {
|
||||
// Copilot supports Responses for GPT-5 class models, except mini variants
|
||||
// which still need the chat-completions endpoint.
|
||||
const match = /^gpt-(\d+)/.exec(modelID)
|
||||
if (!match) return false
|
||||
return Number(match[1]) >= 5 && !modelID.startsWith("gpt-5-mini")
|
||||
}
|
||||
|
||||
export const GithubCopilotPlugin = define({
|
||||
id: "opencode.provider.github-copilot",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
|
|
@ -37,9 +29,21 @@ export const GithubCopilotPlugin = define({
|
|||
evt.language = evt.sdk.languageModel(evt.model.api.id)
|
||||
return
|
||||
}
|
||||
evt.language = shouldUseResponses(evt.model.api.id)
|
||||
? evt.sdk.responses(evt.model.api.id)
|
||||
: evt.sdk.chat(evt.model.api.id)
|
||||
if (evt.options.endpoint === "responses" && evt.sdk.responses) {
|
||||
evt.language = evt.sdk.responses(evt.model.api.id)
|
||||
return
|
||||
}
|
||||
if (evt.options.endpoint === "chat" && evt.sdk.chat) {
|
||||
evt.language = evt.sdk.chat(evt.model.api.id)
|
||||
return
|
||||
}
|
||||
const match = /^gpt-(\d+)/.exec(evt.model.api.id)
|
||||
// Copilot supports Responses for GPT-5 class models, except mini variants
|
||||
// which still need the chat-completions endpoint.
|
||||
evt.language =
|
||||
match && Number(match[1]) >= 5 && !evt.model.api.id.startsWith("gpt-5-mini") && evt.sdk.responses
|
||||
? evt.sdk.responses(evt.model.api.id)
|
||||
: evt.sdk.chat(evt.model.api.id)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -18,39 +18,27 @@ const TOOL_OUTPUT_MAX_CHARS = 2_000
|
|||
const SUMMARY_OUTPUT_TOKENS = 4_096
|
||||
const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside <template> and keep the section order unchanged. Do not include the <template> tags in your response.
|
||||
<template>
|
||||
## Goal
|
||||
- [single-sentence task summary]
|
||||
## Objective
|
||||
- [one or two brief sentences describing what the user is trying to accomplish]
|
||||
|
||||
## Constraints & Preferences
|
||||
- [user constraints, preferences, specs, or "(none)"]
|
||||
## Important Details
|
||||
- [constraints/preferences, decisions and why, important facts/assumptions, exact context needed to continue, or "(none)"]
|
||||
|
||||
## Progress
|
||||
### Done
|
||||
- [completed work or "(none)"]
|
||||
## Work State
|
||||
- Completed: [finished work, verified facts, or changes made; otherwise "(none)"]
|
||||
- Active: [current work, partial changes, or investigation state; otherwise "(none)"]
|
||||
- Blocked: [blockers, failing commands, or unknowns; otherwise "(none)"]
|
||||
|
||||
### In Progress
|
||||
- [current work or "(none)"]
|
||||
|
||||
### Blocked
|
||||
- [blockers or "(none)"]
|
||||
|
||||
## Key Decisions
|
||||
- [decision and why, or "(none)"]
|
||||
|
||||
## Next Steps
|
||||
- [ordered next actions or "(none)"]
|
||||
|
||||
## Critical Context
|
||||
- [important technical facts, errors, open questions, or "(none)"]
|
||||
|
||||
## Relevant Files
|
||||
- [file or directory path: why it matters, or "(none)"]
|
||||
## Next Move
|
||||
1. [immediate concrete action, or "(none)"]
|
||||
2. [next action if known, or "(none)"]
|
||||
</template>
|
||||
|
||||
Rules:
|
||||
- Keep every section, even when empty.
|
||||
- Use terse bullets, not prose paragraphs.
|
||||
- Preserve exact file paths, commands, error strings, and identifiers when known.
|
||||
- Preserve exact file paths, symbols, commands, error strings, URLs, and identifiers when known.
|
||||
- Put relevant files and symbols inside the section where they matter; do not add extra sections.
|
||||
- Do not mention the summary process or that context was compacted.`
|
||||
|
||||
type Settings = {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { Config } from "../../config"
|
|||
import { Database } from "../../database/database"
|
||||
import { EventV2 } from "../../event"
|
||||
import { Location } from "../../location"
|
||||
import { PermissionV2 } from "../../permission"
|
||||
import { Instructions } from "../../instructions/index"
|
||||
import { InstructionBuiltIns } from "../../instructions/builtins"
|
||||
import { InstructionDiscovery } from "../../instruction-discovery"
|
||||
|
|
@ -149,8 +150,13 @@ const layer = Layer.effect(
|
|||
const awaitToolFibers = (fibers: FiberSet.FiberSet<void, ToolOutputStore.Error>) =>
|
||||
Effect.raceFirst(FiberSet.join(fibers), FiberSet.awaitEmpty(fibers))
|
||||
|
||||
const isQuestionCancelled = (cause: Cause.Cause<unknown>) =>
|
||||
cause.reasons.some((reason) => Cause.isDieReason(reason) && reason.defect instanceof QuestionTool.CancelledError)
|
||||
// Declining an interactive prompt halts the drain instead of becoming model-facing tool output.
|
||||
const isUserDeclined = (cause: Cause.Cause<unknown>) =>
|
||||
cause.reasons.some(
|
||||
(reason) =>
|
||||
Cause.isDieReason(reason) &&
|
||||
(reason.defect instanceof PermissionV2.DeclinedError || reason.defect instanceof QuestionTool.CancelledError),
|
||||
)
|
||||
|
||||
const loadInstructions = (agent: AgentV2.Selection, sessionID: SessionSchema.ID) =>
|
||||
Effect.all(
|
||||
|
|
@ -340,13 +346,13 @@ const layer = Layer.effect(
|
|||
if (streamInterrupted) yield* FiberSet.clear(toolFibers)
|
||||
const settled = yield* restore(awaitToolFibers(toolFibers)).pipe(Effect.exit)
|
||||
const toolsInterrupted = settled._tag === "Failure" && Cause.hasInterrupts(settled.cause)
|
||||
const questionCancelled = settled._tag === "Failure" && isQuestionCancelled(settled.cause)
|
||||
const userDeclined = settled._tag === "Failure" && isUserDeclined(settled.cause)
|
||||
|
||||
if (questionCancelled || streamInterrupted || toolsInterrupted) {
|
||||
if (userDeclined || streamInterrupted || toolsInterrupted) {
|
||||
yield* FiberSet.clear(toolFibers)
|
||||
yield* serialized(publisher.failUnsettledTools("Tool execution interrupted"))
|
||||
yield* serialized(publisher.failAssistant("Step interrupted"))
|
||||
if (questionCancelled) return yield* Effect.interrupt
|
||||
if (userDeclined) return yield* Effect.interrupt
|
||||
}
|
||||
// A settled tool fiber failure is one of two things. A defect from a tool
|
||||
// implementation becomes a failed tool call the model can read, and the step still
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue