mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-28 12:36:15 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84481f6b99 |
+43
-48
@@ -319,16 +319,15 @@ export const layer = (options?: Options) =>
|
||||
}
|
||||
})
|
||||
|
||||
const reload = Effect.fn("Config.reload")(() =>
|
||||
reloadLock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const next = yield* discover()
|
||||
yield* reconcile(next)
|
||||
if (isDeepStrictEqual(configs, next)) return
|
||||
configs = next
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
}),
|
||||
),
|
||||
const reload = Effect.fn("Config.reload")(
|
||||
function* () {
|
||||
const next = yield* discover()
|
||||
yield* reconcile(next)
|
||||
if (isDeepStrictEqual(configs, next)) return
|
||||
configs = next
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
},
|
||||
(effect) => reloadLock.withPermit(effect),
|
||||
)
|
||||
|
||||
yield* Stream.fromPubSub(updates).pipe(
|
||||
@@ -379,47 +378,43 @@ export const layer = (options?: Options) =>
|
||||
)
|
||||
yield* reconcile(initial)
|
||||
|
||||
const update = Effect.fn("Config.update")((mutate: (draft: Draft<Info>) => void) =>
|
||||
reloadLock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
// TODO: Replace entry-order selection with an explicit config scope/target model.
|
||||
const document = configs.find((entry) => entry.type === "document" && entry.path !== undefined)
|
||||
if (!document || document.type !== "document" || !document.path)
|
||||
return yield* Effect.fail(new UpdateError({ message: "No editable config document found" }))
|
||||
const next = yield* Effect.try({
|
||||
try: () => produce(document.info, mutate),
|
||||
catch: (cause) => new UpdateError({ message: "Config update failed", cause }),
|
||||
})
|
||||
const edits = changes(document.info, next)
|
||||
if (!edits.length) return document.info
|
||||
const text = yield* fs
|
||||
.readFileString(document.path)
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
(cause) => new UpdateError({ message: `Failed to read config: ${document.path}`, cause }),
|
||||
),
|
||||
)
|
||||
const updated = edits.reduce(
|
||||
(text, edit) =>
|
||||
applyEdits(
|
||||
text,
|
||||
modify(text, edit.path, edit.value, { formattingOptions: { tabSize: 2, insertSpaces: true } }),
|
||||
),
|
||||
text,
|
||||
)
|
||||
const info = yield* parseInfo(updated, document.path)
|
||||
if (!info)
|
||||
return yield* Effect.fail(new UpdateError({ message: `Invalid config update: ${document.path}` }))
|
||||
const temporary = document.path + ".tmp"
|
||||
yield* fs.writeFileString(temporary, updated.endsWith("\n") ? updated : updated + "\n").pipe(
|
||||
Effect.andThen(fs.rename(temporary, document.path)),
|
||||
const update = Effect.fn("Config.update")(
|
||||
function* (mutate: (draft: Draft<Info>) => void) {
|
||||
// TODO: Replace entry-order selection with an explicit config scope/target model.
|
||||
const document = configs.find((entry) => entry.type === "document" && entry.path !== undefined)
|
||||
if (!document || document.type !== "document" || !document.path)
|
||||
return yield* Effect.fail(new UpdateError({ message: "No editable config document found" }))
|
||||
const next = yield* Effect.try({
|
||||
try: () => produce(document.info, mutate),
|
||||
catch: (cause) => new UpdateError({ message: "Config update failed", cause }),
|
||||
})
|
||||
const edits = changes(document.info, next)
|
||||
if (!edits.length) return document.info
|
||||
const text = yield* fs
|
||||
.readFileString(document.path)
|
||||
.pipe(
|
||||
Effect.mapError(
|
||||
(cause) => new UpdateError({ message: `Failed to write config: ${document.path}`, cause }),
|
||||
(cause) => new UpdateError({ message: `Failed to read config: ${document.path}`, cause }),
|
||||
),
|
||||
)
|
||||
return info
|
||||
}),
|
||||
),
|
||||
const updated = edits.reduce(
|
||||
(text, edit) =>
|
||||
applyEdits(
|
||||
text,
|
||||
modify(text, edit.path, edit.value, { formattingOptions: { tabSize: 2, insertSpaces: true } }),
|
||||
),
|
||||
text,
|
||||
)
|
||||
const info = yield* parseInfo(updated, document.path)
|
||||
if (!info) return yield* Effect.fail(new UpdateError({ message: `Invalid config update: ${document.path}` }))
|
||||
const temporary = document.path + ".tmp"
|
||||
yield* fs.writeFileString(temporary, updated.endsWith("\n") ? updated : updated + "\n").pipe(
|
||||
Effect.andThen(fs.rename(temporary, document.path)),
|
||||
Effect.mapError((cause) => new UpdateError({ message: `Failed to write config: ${document.path}`, cause })),
|
||||
)
|
||||
return info
|
||||
},
|
||||
(effect, _mutate) => reloadLock.withPermit(effect),
|
||||
)
|
||||
|
||||
return Service.of({
|
||||
|
||||
@@ -74,26 +74,25 @@ export const Plugin = define({
|
||||
),
|
||||
)
|
||||
|
||||
const refresh = Effect.fn("ConfigInstructionPlugin.refresh")(function* (file?: string) {
|
||||
yield* lock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const sources = yield* Effect.all({
|
||||
global: isolate("global", globalSource()),
|
||||
project: isolate("project", projectSource()),
|
||||
})
|
||||
loaded.current =
|
||||
Array.isArray(sources.global) && Array.isArray(sources.project)
|
||||
? { type: "available", files: [...sources.global, ...sources.project] }
|
||||
: { type: "unavailable" }
|
||||
if (!file) return
|
||||
yield* Effect.logDebug("instructions rescanned", {
|
||||
file,
|
||||
instructions:
|
||||
loaded.current.type === "available" ? loaded.current.files.map((item) => item.path) : "unavailable",
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
const refresh = Effect.fn("ConfigInstructionPlugin.refresh")(
|
||||
function* (file?: string) {
|
||||
const sources = yield* Effect.all({
|
||||
global: isolate("global", globalSource()),
|
||||
project: isolate("project", projectSource()),
|
||||
})
|
||||
loaded.current =
|
||||
Array.isArray(sources.global) && Array.isArray(sources.project)
|
||||
? { type: "available", files: [...sources.global, ...sources.project] }
|
||||
: { type: "unavailable" }
|
||||
if (!file) return
|
||||
yield* Effect.logDebug("instructions rescanned", {
|
||||
file,
|
||||
instructions:
|
||||
loaded.current.type === "available" ? loaded.current.files.map((item) => item.path) : "unavailable",
|
||||
})
|
||||
},
|
||||
(effect, ..._args: [file?: string]) => lock.withPermit(effect),
|
||||
)
|
||||
|
||||
yield* Stream.fromPubSub(changes).pipe(
|
||||
Stream.runForEach((file) => refresh(file).pipe(Effect.andThen(discovery.reload()))),
|
||||
|
||||
@@ -151,26 +151,25 @@ export const Plugin = define({
|
||||
return skills
|
||||
})
|
||||
|
||||
const refresh = Effect.fn("ConfigSkillPlugin.refresh")(function* (file?: string) {
|
||||
yield* lock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
yield* FiberMap.clear(watches)
|
||||
const skills = new Map<Skill.ID, Skill.Info>()
|
||||
const current = sources()
|
||||
for (const source of current) {
|
||||
for (const skill of yield* load(source)) skills.set(skill.id, skill)
|
||||
}
|
||||
loaded.skills = Array.from(skills.values())
|
||||
if (file) {
|
||||
yield* Effect.logInfo("skills rescanned", {
|
||||
file,
|
||||
sources: current.map(Skill.Source.key),
|
||||
skills: loaded.skills.map((skill) => skill.id),
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
const refresh = Effect.fn("ConfigSkillPlugin.refresh")(
|
||||
function* (file?: string) {
|
||||
yield* FiberMap.clear(watches)
|
||||
const skills = new Map<Skill.ID, Skill.Info>()
|
||||
const current = sources()
|
||||
for (const source of current) {
|
||||
for (const skill of yield* load(source)) skills.set(skill.id, skill)
|
||||
}
|
||||
loaded.skills = Array.from(skills.values())
|
||||
if (file) {
|
||||
yield* Effect.logInfo("skills rescanned", {
|
||||
file,
|
||||
sources: current.map(Skill.Source.key),
|
||||
skills: loaded.skills.map((skill) => skill.id),
|
||||
})
|
||||
}
|
||||
},
|
||||
(effect, ..._args: [file?: string]) => lock.withPermit(effect),
|
||||
)
|
||||
|
||||
yield* Stream.fromPubSub(changes).pipe(
|
||||
Stream.runForEach((file) => refresh(file).pipe(Effect.andThen(ctx.skill.reload()))),
|
||||
|
||||
@@ -378,117 +378,109 @@ const layer = Layer.effect(
|
||||
}
|
||||
|
||||
const settle = Effect.fnUntraced(function* (attemptID: AttemptID, exit: Exit.Exit<Credential.OAuth, unknown>) {
|
||||
return yield* Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
|
||||
const match = current.get(attemptID)
|
||||
if (!match || match.status !== "pending" || match.persisting) return [undefined, current]
|
||||
const next = Exit.isSuccess(exit)
|
||||
? { ...match, persisting: true }
|
||||
: {
|
||||
status: "failed" as const,
|
||||
integrationID: match.integrationID,
|
||||
message: message(exit.cause),
|
||||
time: match.time,
|
||||
removeAt: now + terminalRetention,
|
||||
}
|
||||
return [match, new Map(current).set(attemptID, next)]
|
||||
})
|
||||
if (!attempt) return
|
||||
if (Exit.isFailure(exit)) {
|
||||
yield* close(attempt.scope)
|
||||
return
|
||||
}
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
|
||||
const match = current.get(attemptID)
|
||||
if (!match || match.status !== "pending" || match.persisting) return [undefined, current]
|
||||
const next = Exit.isSuccess(exit)
|
||||
? { ...match, persisting: true }
|
||||
: {
|
||||
status: "failed" as const,
|
||||
integrationID: match.integrationID,
|
||||
message: message(exit.cause),
|
||||
time: match.time,
|
||||
removeAt: now + terminalRetention,
|
||||
}
|
||||
return [match, new Map(current).set(attemptID, next)]
|
||||
})
|
||||
if (!attempt) return
|
||||
if (Exit.isFailure(exit)) {
|
||||
yield* close(attempt.scope)
|
||||
return
|
||||
}
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const implementation = state
|
||||
.get()
|
||||
.integrations.get(attempt.integrationID)
|
||||
?.implementations.get(attempt.methodID)
|
||||
const persistence = yield* Effect.sync(() => attempt.label ?? implementation?.label?.(exit.value)).pipe(
|
||||
Effect.flatMap((label) =>
|
||||
createCredential({
|
||||
integrationID: attempt.integrationID,
|
||||
label,
|
||||
value: exit.value,
|
||||
}),
|
||||
),
|
||||
Effect.asVoid,
|
||||
Effect.exit,
|
||||
)
|
||||
const settledAt = yield* Clock.currentTimeMillis
|
||||
const terminal: TerminalAttempt = Exit.isSuccess(persistence)
|
||||
? {
|
||||
status: "complete",
|
||||
integrationID: attempt.integrationID,
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
: {
|
||||
status: "failed",
|
||||
integrationID: attempt.integrationID,
|
||||
message: message(persistence.cause),
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
// Persisting attempts cannot be cancelled, expired, or claimed again.
|
||||
yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal))
|
||||
if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause)
|
||||
}).pipe(Effect.ensuring(close(attempt.scope)))
|
||||
}),
|
||||
)
|
||||
})
|
||||
yield* Effect.gen(function* () {
|
||||
const implementation = state
|
||||
.get()
|
||||
.integrations.get(attempt.integrationID)
|
||||
?.implementations.get(attempt.methodID)
|
||||
const persistence = yield* Effect.sync(() => attempt.label ?? implementation?.label?.(exit.value)).pipe(
|
||||
Effect.flatMap((label) =>
|
||||
createCredential({
|
||||
integrationID: attempt.integrationID,
|
||||
label,
|
||||
value: exit.value,
|
||||
}),
|
||||
),
|
||||
Effect.asVoid,
|
||||
Effect.exit,
|
||||
)
|
||||
const settledAt = yield* Clock.currentTimeMillis
|
||||
const terminal: TerminalAttempt = Exit.isSuccess(persistence)
|
||||
? {
|
||||
status: "complete",
|
||||
integrationID: attempt.integrationID,
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
: {
|
||||
status: "failed",
|
||||
integrationID: attempt.integrationID,
|
||||
message: message(persistence.cause),
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
// Persisting attempts cannot be cancelled, expired, or claimed again.
|
||||
yield* SynchronizedRef.update(attempts, (current) => new Map(current).set(attemptID, terminal))
|
||||
if (Exit.isFailure(persistence)) yield* Effect.failCause(persistence.cause)
|
||||
}).pipe(Effect.ensuring(close(attempt.scope)))
|
||||
}, Effect.uninterruptible)
|
||||
|
||||
const settleCommand = Effect.fnUntraced(function* (attemptID: AttemptID, exit: Exit.Exit<string, unknown>) {
|
||||
return yield* Effect.uninterruptible(
|
||||
Effect.gen(function* () {
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
const attempt = yield* SynchronizedRef.modify(commandAttempts, (current) => {
|
||||
const match = current.get(attemptID)
|
||||
if (!match || match.status !== "pending" || match.persisting) return [undefined, current]
|
||||
const next = Exit.isSuccess(exit)
|
||||
? { ...match, persisting: true }
|
||||
: {
|
||||
status: "failed" as const,
|
||||
integrationID: match.integrationID,
|
||||
message: message(exit.cause),
|
||||
time: match.time,
|
||||
removeAt: now + terminalRetention,
|
||||
}
|
||||
return [match, new Map(current).set(attemptID, next)]
|
||||
})
|
||||
if (!attempt) return
|
||||
if (Exit.isFailure(exit)) {
|
||||
yield* close(attempt.scope)
|
||||
return
|
||||
}
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
const attempt = yield* SynchronizedRef.modify(commandAttempts, (current) => {
|
||||
const match = current.get(attemptID)
|
||||
if (!match || match.status !== "pending" || match.persisting) return [undefined, current]
|
||||
const next = Exit.isSuccess(exit)
|
||||
? { ...match, persisting: true }
|
||||
: {
|
||||
status: "failed" as const,
|
||||
integrationID: match.integrationID,
|
||||
message: message(exit.cause),
|
||||
time: match.time,
|
||||
removeAt: now + terminalRetention,
|
||||
}
|
||||
return [match, new Map(current).set(attemptID, next)]
|
||||
})
|
||||
if (!attempt) return
|
||||
if (Exit.isFailure(exit)) {
|
||||
yield* close(attempt.scope)
|
||||
return
|
||||
}
|
||||
|
||||
const persistence = yield* createCredential({
|
||||
const persistence = yield* createCredential({
|
||||
integrationID: attempt.integrationID,
|
||||
label: attempt.label,
|
||||
value: Credential.Key.make({ type: "key", key: exit.value }),
|
||||
}).pipe(Effect.asVoid, Effect.exit)
|
||||
const settledAt = yield* Clock.currentTimeMillis
|
||||
const terminal: TerminalCommandAttempt = Exit.isSuccess(persistence)
|
||||
? {
|
||||
status: "complete",
|
||||
integrationID: attempt.integrationID,
|
||||
label: attempt.label,
|
||||
value: Credential.Key.make({ type: "key", key: exit.value }),
|
||||
}).pipe(Effect.asVoid, Effect.exit)
|
||||
const settledAt = yield* Clock.currentTimeMillis
|
||||
const terminal: TerminalCommandAttempt = Exit.isSuccess(persistence)
|
||||
? {
|
||||
status: "complete",
|
||||
integrationID: attempt.integrationID,
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
: {
|
||||
status: "failed",
|
||||
integrationID: attempt.integrationID,
|
||||
message: message(persistence.cause),
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
yield* SynchronizedRef.update(commandAttempts, (current) => new Map(current).set(attemptID, terminal))
|
||||
yield* close(attempt.scope)
|
||||
}),
|
||||
)
|
||||
})
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
: {
|
||||
status: "failed",
|
||||
integrationID: attempt.integrationID,
|
||||
message: message(persistence.cause),
|
||||
time: attempt.time,
|
||||
removeAt: settledAt + terminalRetention,
|
||||
}
|
||||
yield* SynchronizedRef.update(commandAttempts, (current) => new Map(current).set(attemptID, terminal))
|
||||
yield* close(attempt.scope)
|
||||
}, Effect.uninterruptible)
|
||||
|
||||
const scrub = Effect.fnUntraced(function* () {
|
||||
const now = yield* Clock.currentTimeMillis
|
||||
|
||||
@@ -124,62 +124,58 @@ const layer = Layer.effect(
|
||||
return entries
|
||||
})
|
||||
|
||||
const refresh = Effect.fn("WellKnown.refresh")(function* () {
|
||||
return yield* lock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const value = yield* kv.get(sourcesKey)
|
||||
const origins = Schema.is(Sources)(value) ? value : []
|
||||
if (!origins.length) return false
|
||||
const entries = yield* Effect.forEach(origins, loadEntry)
|
||||
const next = new Map(entries.map((entry) => [entry.origin, entry]))
|
||||
const changed = !isDeepStrictEqual(Ref.getUnsafe(cache), next)
|
||||
if (!changed) return false
|
||||
yield* Ref.set(cache, next)
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
return true
|
||||
}),
|
||||
)
|
||||
})
|
||||
const refresh = Effect.fn("WellKnown.refresh")(
|
||||
function* () {
|
||||
const value = yield* kv.get(sourcesKey)
|
||||
const origins = Schema.is(Sources)(value) ? value : []
|
||||
if (!origins.length) return false
|
||||
const entries = yield* Effect.forEach(origins, loadEntry)
|
||||
const next = new Map(entries.map((entry) => [entry.origin, entry]))
|
||||
const changed = !isDeepStrictEqual(Ref.getUnsafe(cache), next)
|
||||
if (!changed) return false
|
||||
yield* Ref.set(cache, next)
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
return true
|
||||
},
|
||||
(effect) => lock.withPermit(effect),
|
||||
)
|
||||
|
||||
return Service.of({
|
||||
entries: load,
|
||||
snapshot: () => Array.from(Ref.getUnsafe(cache).values()),
|
||||
refresh,
|
||||
add: Effect.fn("WellKnown.add")(function* (value) {
|
||||
return yield* lock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const origin = value.replace(/\/+$/, "")
|
||||
const entry = yield* loadEntry(origin)
|
||||
if (!entry.manifest.auth)
|
||||
return yield* Effect.fail(new Error(`No authentication method found at ${origin}`))
|
||||
const sources = yield* kv.get(sourcesKey)
|
||||
const origins = Schema.is(Sources)(sources) ? sources : []
|
||||
yield* kv.set(sourcesKey, Array.from(new Set([...origins, origin])))
|
||||
yield* Ref.update(cache, (current) => new Map(current).set(origin, entry))
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
return entry
|
||||
}),
|
||||
)
|
||||
}),
|
||||
remove: Effect.fn("WellKnown.remove")(function* (value) {
|
||||
yield* lock.withPermit(
|
||||
Effect.gen(function* () {
|
||||
const origin = value.replace(/\/+$/, "")
|
||||
const sources = yield* kv.get(sourcesKey)
|
||||
const origins = Schema.is(Sources)(sources) ? sources : []
|
||||
yield* kv.set(
|
||||
sourcesKey,
|
||||
origins.filter((item) => item !== origin),
|
||||
)
|
||||
yield* Ref.update(cache, (current) => {
|
||||
const next = new Map(current)
|
||||
next.delete(origin)
|
||||
return next
|
||||
})
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
}),
|
||||
)
|
||||
}),
|
||||
add: Effect.fn("WellKnown.add")(
|
||||
function* (value) {
|
||||
const origin = value.replace(/\/+$/, "")
|
||||
const entry = yield* loadEntry(origin)
|
||||
if (!entry.manifest.auth) return yield* Effect.fail(new Error(`No authentication method found at ${origin}`))
|
||||
const sources = yield* kv.get(sourcesKey)
|
||||
const origins = Schema.is(Sources)(sources) ? sources : []
|
||||
yield* kv.set(sourcesKey, Array.from(new Set([...origins, origin])))
|
||||
yield* Ref.update(cache, (current) => new Map(current).set(origin, entry))
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
return entry
|
||||
},
|
||||
(effect, _value) => lock.withPermit(effect),
|
||||
),
|
||||
remove: Effect.fn("WellKnown.remove")(
|
||||
function* (value) {
|
||||
const origin = value.replace(/\/+$/, "")
|
||||
const sources = yield* kv.get(sourcesKey)
|
||||
const origins = Schema.is(Sources)(sources) ? sources : []
|
||||
yield* kv.set(
|
||||
sourcesKey,
|
||||
origins.filter((item) => item !== origin),
|
||||
)
|
||||
yield* Ref.update(cache, (current) => {
|
||||
const next = new Map(current)
|
||||
next.delete(origin)
|
||||
return next
|
||||
})
|
||||
yield* bus.publish(Event.Updated, {})
|
||||
},
|
||||
(effect, _value) => lock.withPermit(effect),
|
||||
),
|
||||
resolve: Effect.fn("WellKnown.resolveEntry")((entry, variables) =>
|
||||
resolveEntry(entry, variables).pipe(Effect.provideService(HttpClient.HttpClient, http)),
|
||||
),
|
||||
|
||||
@@ -5,9 +5,7 @@ The Promise plugin API at `@opencode-ai/plugin` is the async/await equivalent of
|
||||
- `hook` installs behavior at an OpenCode extension point.
|
||||
- `reload` reruns every transform hook for a stateful domain.
|
||||
|
||||
The Promise API uses Promises instead of Effects for setup, runtime hook
|
||||
callbacks, hook registration, `reload`, and `Registration.dispose`. Transform
|
||||
draft callbacks remain synchronous.
|
||||
The only difference from the Effect API is the async boundary: hook callbacks, hook registration, `reload`, and `Registration.dispose` use Promises instead of Effects.
|
||||
|
||||
## Defining A Plugin
|
||||
|
||||
@@ -48,15 +46,12 @@ await registration.dispose()
|
||||
|
||||
## Transform Hooks
|
||||
|
||||
Transform hooks contribute to stateful domains. The draft editor is synchronous,
|
||||
so load asynchronous data before registering a transform or reloading its domain:
|
||||
Transform hooks contribute to stateful domains. The draft editor is synchronous; the callback may be `async` when it needs to await other work:
|
||||
|
||||
```ts
|
||||
const description = await loadReviewerDescription()
|
||||
|
||||
await ctx.agent.transform((agent) => {
|
||||
agent.update("reviewer", (item) => {
|
||||
item.description = description
|
||||
item.description = "Reviews code for regressions"
|
||||
item.mode = "subagent"
|
||||
})
|
||||
})
|
||||
@@ -69,12 +64,8 @@ ctx.agent.transform
|
||||
ctx.catalog.transform
|
||||
ctx.command.transform
|
||||
ctx.integration.transform
|
||||
ctx.mcp.transform
|
||||
ctx.reference.transform
|
||||
ctx.skill.transform
|
||||
ctx.tool.transform
|
||||
ctx.vcs.transform
|
||||
ctx.websearch.transform
|
||||
```
|
||||
|
||||
## Runtime Hooks
|
||||
@@ -90,7 +81,7 @@ await ctx.aisdk.hook("sdk", async (event) => {
|
||||
|
||||
await ctx.aisdk.hook("language", (event) => {
|
||||
if (event.model.providerID !== "xai") return
|
||||
event.language = event.sdk.responses(event.model.modelID)
|
||||
event.language = event.sdk.responses(event.model.api.id)
|
||||
})
|
||||
```
|
||||
|
||||
@@ -103,15 +94,14 @@ await ctx.session.hook("context", (event) => {
|
||||
})
|
||||
```
|
||||
|
||||
Promise tools use complete executable tool values with async executors:
|
||||
Promise tools use executable tool values with async executors. Registration
|
||||
supplies the tool's name and options separately:
|
||||
|
||||
```ts
|
||||
import { Schema } from "effect"
|
||||
|
||||
await ctx.tool.transform((tools) => {
|
||||
tools.add({
|
||||
name: "echo",
|
||||
options: { codemode: false },
|
||||
tools.add("echo", {
|
||||
description: "Echo text",
|
||||
input: Schema.Struct({ text: Schema.String }),
|
||||
output: Schema.Struct({ text: Schema.String }),
|
||||
@@ -142,10 +132,6 @@ ctx.agent.reload()
|
||||
ctx.catalog.reload()
|
||||
ctx.command.reload()
|
||||
ctx.integration.reload()
|
||||
ctx.mcp.reload()
|
||||
ctx.reference.reload()
|
||||
ctx.skill.reload()
|
||||
ctx.tool.reload()
|
||||
ctx.vcs.reload()
|
||||
ctx.websearch.reload()
|
||||
```
|
||||
|
||||
@@ -31,9 +31,7 @@ Registrations are owned by the plugin scope. Closing the scope removes them auto
|
||||
|
||||
## Transform Hooks
|
||||
|
||||
Transform hooks contribute to stateful domains. Their draft callbacks are
|
||||
synchronous, so load effectful data before registering a transform or reloading
|
||||
its domain:
|
||||
Transform hooks contribute to stateful domains:
|
||||
|
||||
```ts
|
||||
yield *
|
||||
@@ -54,12 +52,8 @@ ctx.agent.transform
|
||||
ctx.catalog.transform
|
||||
ctx.command.transform
|
||||
ctx.integration.transform
|
||||
ctx.mcp.transform
|
||||
ctx.reference.transform
|
||||
ctx.skill.transform
|
||||
ctx.tool.transform
|
||||
ctx.vcs.transform
|
||||
ctx.websearch.transform
|
||||
```
|
||||
|
||||
## Runtime Hooks
|
||||
@@ -78,12 +72,10 @@ yield *
|
||||
)
|
||||
|
||||
yield *
|
||||
ctx.aisdk.hook("language", (event) =>
|
||||
Effect.sync(() => {
|
||||
if (event.model.providerID !== "xai") return
|
||||
event.language = event.sdk.responses(event.model.modelID)
|
||||
}),
|
||||
)
|
||||
ctx.aisdk.hook("language", (event) => {
|
||||
if (event.model.providerID !== "xai") return
|
||||
event.language = event.sdk.responses(event.model.api.id)
|
||||
})
|
||||
```
|
||||
|
||||
Hooks run sequentially in registration order. Later hooks observe mutations made by earlier hooks.
|
||||
@@ -125,10 +117,6 @@ ctx.agent.reload()
|
||||
ctx.catalog.reload()
|
||||
ctx.command.reload()
|
||||
ctx.integration.reload()
|
||||
ctx.mcp.reload()
|
||||
ctx.reference.reload()
|
||||
ctx.skill.reload()
|
||||
ctx.tool.reload()
|
||||
ctx.vcs.reload()
|
||||
ctx.websearch.reload()
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user