feat(agent-manager): allow sessions to move their worktree between sections or ungroup (#12815)

* feat(agent-manager): allow sessions to move their worktree between sections or ungroup

Add a new  to the  tool so a session can
reassign its own worktree to another section or ungroup it by passing
. The move operation validates the target section and
the session's worktree, then pushes refreshed state to the panel.

Also tighten the model-facing contract so the list-to-move workflow is
unambiguous:  is required first, its result is the
source of truth for section and session IDs, and direct edits to
 are rejected by , , and
 with an explicit pointer to the  tool.

- New / in the Agent Manager protocol
- New  domain function with section/session validation
- New  OpenAPI hook so  stays
  nullable in the generated SDK
- Tool schema descriptions now spell out the list-first workflow and
  the no-direct-edit rule
- List output now includes an instruction block and is pretty-printed
- Optional nullable Task fields tolerate  from models
- Focused CLI tests, bridge unit test, and protection test

* chore: remove local PR screenshot

* style(vscode): format agent manager orchestration

* fix(agent-manager): protect state paths on Windows
This commit is contained in:
Marius
2026-08-03 16:32:01 +02:00
committed by GitHub
parent 6ae16f9d62
commit 3d4294e3bb
20 changed files with 660 additions and 262 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---
Allow Agent Manager sessions to move their worktree between sections or ungroup it through the `agent_manager` tool.
@@ -159,9 +159,9 @@ Each request can include 1-20 tasks. Each task must include at least one of `pro
The companion `agent_manager_models` tool searches models and their supported reasoning variants on demand. Results are grouped by model name (with the offering providers listed for reference) and limited to 20 per call, so the full catalog is never added to the conversation context.
The same tool also manages existing sessions. It can return a compact overview of sections, worktrees, and local sessions, send a prompt to one managed session, or stop a managed session. Stopping aborts the session's active work and removes it from the panel, just like closing the session tab.
The same tool also manages existing sessions. It can return an overview of sections, worktrees, and local sessions, send a prompt to one managed session, stop a managed session, or move a session's worktree into a section. The overview includes section IDs, each section's assigned worktrees, worktree IDs, and session IDs. Use those exact IDs for a subsequent move. Moving accepts a section ID from the overview, or `null` to ungroup the worktree. Moving a session moves its whole worktree, including multi-version siblings. Local sessions cannot be assigned to a section. Stopping aborts the session's active work and removes it from the panel, just like closing the session tab.
The tool uses the `agent_manager` permission. Approval prompts are scoped to the requested capability, so approving `worktree` does not automatically approve `local`, an overview, or a targeted prompt. Prompting an existing managed session requires an explicit `prompt` approval the first time, even if Agent Manager session creation was previously approved broadly. Stopping a session likewise requires an explicit `stop` approval.
The tool uses the `agent_manager` permission. Approval prompts are scoped to the requested capability, so approving `worktree` does not automatically approve `local`, an overview, or a targeted prompt. Prompting an existing managed session requires an explicit `prompt` approval the first time, even if Agent Manager session creation was previously approved broadly. Stopping a session likewise requires an explicit `stop` approval, and moving a worktree requires an explicit `move` approval.
## Sections
@@ -242,6 +242,7 @@ export class AgentManagerProvider implements Disposable {
},
stats: (refresh) => this.statsPoller.snapshot(refresh),
prs: () => this.prBridge.snapshot(),
push: () => this.pushState(),
managed: (id) => this.panelSessions.has(id) || !!this.state?.getSession(id),
close: async (id) => {
await this.onCloseSession(id)
@@ -7,6 +7,7 @@ import type { PRStatus } from "./types"
import type { WorktreeStateManager } from "./WorktreeStateManager"
import {
OrchestrationError,
move,
overview,
prompt,
sameManagedDirectory,
@@ -26,11 +27,13 @@ type Request =
| (RequestBase & { operation: "overview"; filter?: OverviewFilter })
| (RequestBase & { operation: "prompt"; targetSessionID: string; prompt: string })
| (RequestBase & { operation: "stop"; targetSessionID: string })
| (RequestBase & { operation: "move"; targetSessionID: string; sectionID: string | null })
type Result =
| { operation: "overview"; overview: Overview }
| { operation: "prompt"; sessionID: string; delivered: true }
| { operation: "stop"; sessionID: string; stopped: true }
| { operation: "move"; sessionID: string; sectionID: string | null; moved: true }
interface Failure {
code: FailureCode | "cancelled" | "disconnected" | "timeout"
@@ -43,6 +46,7 @@ interface Options {
state(): WorktreeStateManager | undefined
stats(refresh?: boolean): Promise<{ worktrees: WorktreeStats[]; local?: LocalStats }>
prs(): Map<string, PRStatus>
push(): void
managed(sessionID: string): boolean
close(sessionID: string): Promise<void>
log(...args: unknown[]): void
@@ -279,6 +283,19 @@ export class AgentManagerOrchestrationBridge {
if (this.disposed || active.cancelled) return
return { result: { operation: "prompt", sessionID: request.targetSessionID, delivered: true } }
}
if (request.operation === "move") {
move({ state, sessionID: request.targetSessionID, sectionID: request.sectionID })
this.options.push()
if (this.disposed || active.cancelled) return
return {
result: {
operation: "move",
sessionID: request.targetSessionID,
sectionID: request.sectionID,
moved: true,
},
}
}
if (!this.options.managed(request.targetSessionID)) {
throw new OrchestrationError("unknown_session", "The session is not managed by this Agent Manager workspace")
}
@@ -13,6 +13,7 @@ export type FailureCode =
| "host_error"
| "stale_session"
| "unavailable_session"
| "unknown_section"
| "unknown_session"
| "workspace_unavailable"
@@ -362,3 +363,20 @@ export async function prompt(input: {
{ throwOnError: true },
)
}
export function move(input: { state: WorktreeStateManager; sessionID: string; sectionID: string | null }): void {
const session = input.state.getSession(input.sessionID)
if (!session)
throw new OrchestrationError("unknown_session", "The session is not managed by this Agent Manager workspace")
if (!session.worktreeId) {
if (input.sectionID === null) return
throw new OrchestrationError(
"unavailable_session",
"Only sessions attached to a worktree can be assigned to a section",
)
}
if (input.sectionID !== null && !input.state.getSection(input.sectionID)) {
throw new OrchestrationError("unknown_section", "The target section is not managed by this Agent Manager workspace")
}
input.state.moveToSection([session.worktreeId], input.sectionID)
}
@@ -45,6 +45,7 @@ describe("AgentManagerOrchestrationBridge", () => {
const managed = new Set(["ses_target"])
const promptAsync = mock(async () => ({ data: undefined }))
const close = mock(async () => undefined)
const push = mock(() => undefined)
const client = {
session: {
get: mock(async () => ({
@@ -97,6 +98,7 @@ describe("AgentManagerOrchestrationBridge", () => {
state: () => state,
stats: async () => ({ worktrees: [] }),
prs: () => new Map(),
push,
managed: (id) => managed.has(id),
close,
log: () => undefined,
@@ -106,7 +108,7 @@ describe("AgentManagerOrchestrationBridge", () => {
{ id: `event-${value.id}`, type: "kilocode.agent_manager.requested", properties: value } as SSEPayload,
directory,
)
return { bridge, client, close, handlers, lists, managed, promptAsync, rejections, replies, request, status }
return { bridge, client, close, handlers, lists, managed, promptAsync, push, rejections, replies, request, status }
}
const request: AgentManagerRequest = {
@@ -185,6 +187,52 @@ describe("AgentManagerOrchestrationBridge", () => {
test.bridge.dispose()
})
it("moves its own worktree into a section and then ungroups it", async () => {
const test = harness()
const section = state.addSection("Review", null)
test.request(
{
id: "amr_move",
sessionID: "ses_target",
operation: "move",
targetSessionID: "ses_target",
sectionID: section.id,
},
dir,
)
await waitFor(() => test.replies.length === 1)
const worktreeID = state.getSession("ses_target")!.worktreeId!
expect(state.getWorktree(worktreeID)?.sectionId).toBe(section.id)
expect(test.push).toHaveBeenCalledTimes(1)
expect(test.replies[0]).toEqual({
requestID: "amr_move",
directory: dir,
result: { operation: "move", sessionID: "ses_target", sectionID: section.id, moved: true },
})
test.request(
{
id: "amr_ungroup",
sessionID: "ses_target",
operation: "move",
targetSessionID: "ses_target",
sectionID: null,
},
dir,
)
await waitFor(() => test.replies.length === 2)
expect(state.getWorktree(worktreeID)?.sectionId).toBeUndefined()
expect(test.replies[1]).toEqual({
requestID: "amr_ungroup",
directory: dir,
result: { operation: "move", sessionID: "ses_target", sectionID: null, moved: true },
})
test.bridge.dispose()
})
it("stops a live panel session before it is persisted", async () => {
const test = harness()
test.managed.add("ses_live")
@@ -0,0 +1,9 @@
export function assertMutablePath(filepath: string) {
const parts = filepath.split(/[\\/]/)
const file = parts.at(-1)
const dir = parts.at(-2)
if (file !== "agent-manager.json" || ![".kilo", ".kilocode"].includes(dir ?? "")) return
throw new Error(
"Do not edit Agent Manager state directly. Use the agent_manager tool: call action=list to read section and session IDs, then call action=move with the returned IDs.",
)
}
@@ -105,7 +105,14 @@ export const StopRequest = Schema.Struct({
targetSessionID: SessionID,
}).annotate({ identifier: "AgentManagerStopRequest" })
export const Request = Schema.Union([OverviewRequest, PromptRequest, StopRequest]).annotate({
export const MoveRequest = Schema.Struct({
...Base,
operation: Schema.Literal("move"),
targetSessionID: SessionID,
sectionID: Schema.NullOr(ID),
}).annotate({ identifier: "AgentManagerMoveRequest" })
export const Request = Schema.Union([OverviewRequest, PromptRequest, StopRequest, MoveRequest]).annotate({
identifier: "AgentManagerRequest",
})
export type Request = Schema.Schema.Type<typeof Request>
@@ -127,7 +134,14 @@ export const StopResult = Schema.Struct({
stopped: Schema.Literal(true),
}).annotate({ identifier: "AgentManagerStopResult" })
export const Result = Schema.Union([OverviewResult, PromptResult, StopResult]).annotate({
export const MoveResult = Schema.Struct({
operation: Schema.Literal("move"),
sessionID: SessionID,
sectionID: Schema.NullOr(ID),
moved: Schema.Literal(true),
}).annotate({ identifier: "AgentManagerMoveResult" })
export const Result = Schema.Union([OverviewResult, PromptResult, StopResult, MoveResult]).annotate({
identifier: "AgentManagerResult",
})
export type Result = Schema.Schema.Type<typeof Result>
@@ -140,6 +154,7 @@ export const ErrorCode = Schema.Literals([
"stale_session",
"timeout",
"unavailable_session",
"unknown_section",
"unknown_session",
"workspace_unavailable",
])
@@ -2,11 +2,11 @@ import { type Rule } from "./rule"
export namespace AgentManagerPermission {
/**
* Prompting or stopping an existing Agent Manager session has an external side effect.
* Prompting, stopping, or moving an existing Agent Manager session has an external side effect.
* Broad approvals for legacy session creation must not silently grant it.
*/
export function harden(permission: string, pattern: string, rule: Rule): Rule {
if (permission !== "agent_manager" || !["prompt", "stop"].includes(pattern) || rule.action !== "allow") return rule
if (permission !== "agent_manager" || !["prompt", "stop", "move"].includes(pattern) || rule.action !== "allow") return rule
if (rule.permission === "agent_manager" && rule.pattern === pattern) return rule
return { permission, pattern, action: "ask" }
}
@@ -15,14 +15,20 @@ import { matchesQuery } from "./model-search"
import DESCRIPTION from "./agent-manager.txt"
const Task = Schema.Struct({
prompt: Schema.optional(Schema.String).annotate({ description: "Initial prompt to send to the new session" }),
name: Schema.optional(Schema.String).annotate({ description: "Short display name for the Agent Manager card" }),
branchName: Schema.optional(Schema.String).annotate({ description: "Git branch name seed for worktree mode" }),
model: Schema.optional(Schema.String).annotate({
prompt: Schema.optional(Schema.NullOr(Schema.String)).annotate({
description: "Initial prompt to send to the new session",
}),
name: Schema.optional(Schema.NullOr(Schema.String)).annotate({
description: "Short display name for the Agent Manager card",
}),
branchName: Schema.optional(Schema.NullOr(Schema.String)).annotate({
description: "Git branch name seed for worktree mode",
}),
model: Schema.optional(Schema.NullOr(Schema.String)).annotate({
description:
"Optional model override from agent_manager_models (e.g. 'Claude Opus 4.1'). Omit unless the user requests a different model. Agent Manager otherwise inherits the current turn's model. A qualified provider/model ID is also accepted to force a specific provider.",
}),
variant: Schema.optional(Schema.String).annotate({
variant: Schema.optional(Schema.NullOr(Schema.String)).annotate({
description:
"Optional reasoning variant override from agent_manager_models. Specify it without model to override the inherited model's variant. Omit both to inherit the current turn's selection.",
}),
@@ -44,7 +50,7 @@ const StartParams = Schema.Struct({
mode: Schema.Literals(["worktree", "local"]).annotate({
description: "Use worktree for isolated git worktrees, or local for same-directory Agent Manager sessions",
}),
versions: Schema.optional(Schema.Boolean).annotate({
versions: Schema.optional(Schema.NullOr(Schema.Boolean)).annotate({
description:
"Set true only when tasks are alternative versions of the same work to compare. Omit or false for independent sessions.",
}),
@@ -54,17 +60,22 @@ const StartParams = Schema.Struct({
})
const ListParams = Schema.Struct({
action: Schema.Literal("list"),
action: Schema.Literal("list").annotate({
description:
"Read the current Agent Manager sections, worktrees, and sessions before any assignment. This is the source of truth for section and session IDs.",
}),
filter: Schema.optional(
Schema.Struct({
sectionIDs: Schema.optional(Schema.Array(Schema.String).check(Schema.isMaxLength(100))),
states: Schema.optional(
Schema.Array(Schema.Literals(["idle", "busy", "retry", "offline", "waiting"])).check(
Schema.isMaxLength(5),
Schema.NullOr(
Schema.Struct({
sectionIDs: Schema.optional(Schema.Array(Schema.String).check(Schema.isMaxLength(100))),
states: Schema.optional(
Schema.Array(Schema.Literals(["idle", "busy", "retry", "offline", "waiting"])).check(Schema.isMaxLength(5)),
),
),
}),
),
}),
),
).annotate({
description: "Optional list filter. Omit this for an unfiltered overview when discovering assignments.",
}),
})
const PromptParams = Schema.Struct({
@@ -80,16 +91,36 @@ const StopParams = Schema.Struct({
sessionID: SessionID,
})
export const Params = Schema.Union([StartParams, ListParams, PromptParams, StopParams])
const MoveParams = Schema.Struct({
action: Schema.Literal("move").annotate({
description: "Move exactly one managed worktree by targeting one of its session IDs returned by action=list.",
}),
sessionID: SessionID.annotate({
description: "Session ID returned by action=list. Do not use a worktree name, branch, or section name.",
}),
sectionID: Schema.NullOr(Schema.String).annotate({
description: "Section ID returned by action=list. Use null to unassign the worktree from its current section.",
}),
})
export const Params = Schema.Union([StartParams, ListParams, PromptParams, StopParams, MoveParams])
const WireParams = Schema.Struct({
mode: Schema.optional(StartParams.fields.mode),
versions: Schema.optional(StartParams.fields.versions),
tasks: Schema.optional(StartParams.fields.tasks),
action: Schema.optional(Schema.Literals(["list", "prompt", "stop"])),
action: Schema.optional(
Schema.Literals(["list", "prompt", "stop", "move"]).annotate({
description:
"Use list first to discover IDs and assignments. Use move only after list, once per worktree. Never edit .kilo/agent-manager.json for these operations.",
}),
),
filter: Schema.optional(ListParams.fields.filter),
sessionID: Schema.optional(PromptParams.fields.sessionID),
sessionID: Schema.optional(
SessionID.annotate({ description: "For move, use a session ID returned by action=list." }),
),
prompt: Schema.optional(PromptParams.fields.prompt),
sectionID: Schema.optional(MoveParams.fields.sectionID),
})
type Input = Schema.Schema.Type<typeof Task>
@@ -169,9 +200,9 @@ function select(
index: number,
): Selected {
const base = {
...(task.prompt !== undefined ? { prompt: task.prompt } : {}),
...(task.name !== undefined ? { name: task.name } : {}),
...(task.branchName !== undefined ? { branchName: task.branchName } : {}),
...(task.prompt != null ? { prompt: task.prompt } : {}),
...(task.name != null ? { name: task.name } : {}),
...(task.branchName != null ? { branchName: task.branchName } : {}),
}
const value = task.model?.trim()
const variant = task.variant?.trim()
@@ -241,7 +272,7 @@ function select(
export const AgentManagerTool = Tool.define<
typeof Params,
{ action: "start" | "list" | "prompt" | "stop"; requestID?: string; count?: number; sessionID?: string },
{ action: "start" | "list" | "prompt" | "stop" | "move"; requestID?: string; count?: number; sessionID?: string },
AgentManager.Service | Bus.Service | Provider.Service,
"agent_manager"
>(
@@ -250,10 +281,18 @@ export const AgentManagerTool = Tool.define<
const bus = yield* Bus.Service
const host = yield* AgentManager.Service
const provider = yield* Provider.Service
const wire = ToolJsonSchema.fromSchema(WireParams)
const section = wire.properties?.sectionID
if (section && typeof section === "object" && wire.properties) {
wire.properties.sectionID = {
anyOf: [{ type: "string", minLength: 1 }, { type: "null" }],
description: "Section ID returned by action=list. Use null to unassign the worktree from its current section.",
}
}
return {
description: DESCRIPTION,
parameters: Params,
jsonSchema: ToolJsonSchema.fromSchema(WireParams),
jsonSchema: wire,
execute: (params, ctx) =>
Effect.gen(function* () {
if ("action" in params) {
@@ -265,7 +304,7 @@ export const AgentManagerTool = Tool.define<
metadata: { action: "list" },
})
const result = yield* run(
host.request({ operation: "overview", sessionID: ctx.sessionID, filter: params.filter }),
host.request({ operation: "overview", sessionID: ctx.sessionID, filter: params.filter ?? undefined }),
ctx.abort,
)
if (result.operation !== "overview")
@@ -276,7 +315,15 @@ export const AgentManagerTool = Tool.define<
result.overview.sections.reduce((sum, section) => sum + section.worktrees.length, 0)
return {
title: "Agent Manager overview",
output: JSON.stringify(result.overview),
output: JSON.stringify(
{
instructions:
"This overview is the source of truth. Use sections[].id as sectionID and sessions[].id/session.id as sessionID for action=move. Do not edit .kilo/agent-manager.json.",
...result.overview,
},
null,
2,
),
metadata: { action: "list", count },
}
}
@@ -304,26 +351,50 @@ export const AgentManagerTool = Tool.define<
metadata: { action: "prompt", sessionID: result.sessionID },
}
}
if (params.action === "stop") {
yield* ctx.ask({
permission: "agent_manager",
patterns: ["stop"],
always: ["stop"],
metadata: { action: "stop", sessionID: params.sessionID },
})
const result = yield* run(
host.request({
operation: "stop",
sessionID: ctx.sessionID,
targetSessionID: params.sessionID,
}),
ctx.abort,
)
if (result.operation !== "stop")
return yield* Effect.die(new Error("Agent Manager host returned the wrong result type"))
return {
title: "Session stopped",
output: `Stopped Agent Manager session ${result.sessionID} and removed it from Agent Manager.`,
metadata: { action: "stop", sessionID: result.sessionID },
}
}
yield* ctx.ask({
permission: "agent_manager",
patterns: ["stop"],
always: ["stop"],
metadata: { action: "stop", sessionID: params.sessionID },
patterns: ["move"],
always: ["move"],
metadata: { action: "move", sessionID: params.sessionID, sectionID: params.sectionID },
})
const result = yield* run(
host.request({
operation: "stop",
operation: "move",
sessionID: ctx.sessionID,
targetSessionID: params.sessionID,
sectionID: params.sectionID,
}),
ctx.abort,
)
if (result.operation !== "stop")
if (result.operation !== "move")
return yield* Effect.die(new Error("Agent Manager host returned the wrong result type"))
return {
title: "Session stopped",
output: `Stopped Agent Manager session ${result.sessionID} and removed it from Agent Manager.`,
metadata: { action: "stop", sessionID: result.sessionID },
title: "Session moved",
output: `Moved Agent Manager session ${result.sessionID} to ${result.sectionID ?? "Ungrouped"}.`,
metadata: { action: "move", sessionID: result.sessionID, sectionID: result.sectionID },
}
}
@@ -380,7 +451,7 @@ export const AgentManagerTool = Tool.define<
sessionID: ctx.sessionID,
sandboxInheritanceToken,
mode: params.mode,
versions: params.versions,
...(params.versions != null ? { versions: params.versions } : {}),
tasks,
})
@@ -1,6 +1,6 @@
Inspect and orchestrate Agent Manager sessions, or start new sessions, in the VS Code extension.
Inspect and orchestrate Agent Manager sessions, or start new sessions, in the VS Code extension. Use this tool for Agent Manager sections and assignments. Do not edit `.kilo/agent-manager.json` directly: it is persisted UI/recovery state, not the Agent Manager API, and manual patches can overwrite live state.
Use `action: "list"` to inspect the compact Agent Manager overview, `action: "prompt"` to send one instruction to one existing managed session, and `action: "stop"` to stop and remove one managed session. List results include user-defined sections, ungrouped worktrees, and managed local sessions. Optional filters can narrow by section ID or by `idle`, `busy`, `retry`, `offline`, or `waiting` state. Prompting and stopping are targeted only: they do not broadcast or create sessions, and prompting does not wait for the target to finish.
Use `action: "list"` to inspect the Agent Manager overview, `action: "prompt"` to send one instruction to one existing managed session, `action: "stop"` to stop and remove one managed session, and `action: "move"` to move one session's worktree into a section or ungroup it. For any assignment request, the required sequence is: (1) call `agent_manager` with `{ "action": "list" }`; (2) read the returned `sections[].id`, `sections[].worktrees[].session.id` or `sessions[].id`, and `ungrouped[].session.id` or `sessions[].sessions[].id`; (3) call `agent_manager` with `{ "action": "move", "sessionID": "<returned session id>", "sectionID": "<returned section id>" }` once for each worktree; (4) use `sectionID: null` to unassign. Never invent IDs, use section names instead of IDs, or edit `.kilo/agent-manager.json`. The `list` result is the source of truth for IDs and assignments: each `sections` entry includes the section `id`, name, and its assigned `worktrees`; each worktree includes its worktree `id` and its session ID(s) in `session` or `sessions`; `ungrouped` lists worktrees that have no section; and `local.sessions` lists local sessions that cannot be assigned to a section. For `move`, pass the target session's ID as `sessionID` and a section ID as `sectionID`; pass `null` to unassign it. Optional filters can narrow by section ID or by `idle`, `busy`, `retry`, `offline`, or `waiting` state. Prompting, stopping, and moving are targeted only: they do not broadcast or create sessions, and prompting does not wait for the target to finish. Moving a session moves its whole worktree, including multi-version siblings; local sessions cannot be assigned to a section.
To start sessions, keep using the existing `mode` and `tasks` input without an action. Use start mode when the user explicitly asks you to fan out work into Agent Manager, create Agent Manager worktrees, or start multiple Agent Manager sessions for independent tasks.
@@ -116,6 +116,7 @@ function matchLegacyOpenApi(input: Record<string, unknown>) {
}
normalizeComponentNames(spec)
collapseDuplicateComponents(spec)
restoreAgentManagerNulls(spec) // kilocode_change
applyLegacySchemaOverrides(spec)
normalizeComponentDescriptions(spec)
addLegacyErrorSchemas(spec)
@@ -517,6 +518,20 @@ function stripOptionalNull(schema: OpenApiSchema): OpenApiSchema {
return schema
}
// kilocode_change start - preserve nullable Agent Manager move targets in generated SDK schemas
function restoreAgentManagerNulls(spec: OpenApiSpec) {
const schemas = spec.components?.schemas
if (!schemas) return
for (const name of ["AgentManagerMoveRequest", "AgentManagerMoveResult"]) {
const schema = schemas[name]
if (!schema?.properties?.sectionID) continue
schema.properties.sectionID = {
anyOf: [schema.properties.sectionID, { type: "null" }],
}
}
}
// kilocode_change end
function isEmptyObjectUnion(schema: OpenApiSchema) {
const options = schema.anyOf ?? schema.oneOf
return options?.length === 2 && options.some(isBareObjectSchema) && options.some(isBareArraySchema)
@@ -17,6 +17,7 @@ import { ConfigValidation } from "../kilocode/config-validation" // kilocode_cha
import * as EncodedIO from "../kilocode/tool/encoded-io" // kilocode_change
import { Format } from "../format"
import * as Bom from "@/util/bom"
import { assertMutablePath } from "../kilocode/agent-manager/protection" // kilocode_change
export const Parameters = Schema.Struct({
patchText: Schema.String.annotate({ description: "The full patch text that describes all changes to be made" }),
@@ -75,6 +76,7 @@ export const ApplyPatchTool = Tool.define(
for (const hunk of hunks) {
const filePath = path.resolve(instance.directory, hunk.path)
assertMutablePath(filePath) // kilocode_change
yield* assertExternalDirectoryEffect(ctx, filePath)
switch (hunk.type) {
+2
View File
@@ -22,6 +22,7 @@ import { filterDiagnostics } from "./diagnostics" // kilocode_change
import { ConfigValidation } from "../kilocode/config-validation" // kilocode_change
import * as EncodedIO from "../kilocode/tool/encoded-io" // kilocode_change
import * as Encoding from "../kilocode/encoding" // kilocode_change
import { assertMutablePath } from "../kilocode/agent-manager/protection" // kilocode_change
const MAX_DIFF_CONTENT = 500_000 // kilocode_change
@@ -106,6 +107,7 @@ export const EditTool = Tool.define(
const filePath = path.isAbsolute(params.filePath)
? params.filePath
: path.join(instance.directory, params.filePath)
assertMutablePath(filePath) // kilocode_change
yield* assertExternalDirectoryEffect(ctx, filePath)
let diff = ""
+2
View File
@@ -16,6 +16,7 @@ import { assertExternalDirectoryEffect } from "./external-directory"
import { filterDiagnostics } from "./diagnostics" // kilocode_change
import { ConfigValidation } from "../kilocode/config-validation" // kilocode_change
import * as EncodedIO from "../kilocode/tool/encoded-io" // kilocode_change
import { assertMutablePath } from "../kilocode/agent-manager/protection" // kilocode_change
import * as Bom from "@/util/bom"
const MAX_PROJECT_DIAGNOSTICS_FILES = 5
@@ -44,6 +45,7 @@ export const WriteTool = Tool.define(
const filepath = path.isAbsolute(params.filePath)
? params.filePath
: path.join(instance.directory, params.filePath)
assertMutablePath(filepath) // kilocode_change
yield* assertExternalDirectoryEffect(ctx, filepath)
const exists = yield* fs.existsSafe(filepath)
@@ -0,0 +1,18 @@
import { describe, expect, test } from "bun:test"
import { assertMutablePath } from "@/kilocode/agent-manager/protection"
describe("Agent Manager state protection", () => {
test("rejects direct edits to Agent Manager state", () => {
expect(() => assertMutablePath("/workspace/.kilo/agent-manager.json")).toThrow(
"Do not edit Agent Manager state directly",
)
expect(() => assertMutablePath("/workspace/.kilocode/agent-manager.json")).toThrow(
"Do not edit Agent Manager state directly",
)
})
test("allows ordinary project files", () => {
expect(() => assertMutablePath("/workspace/.kilo/settings.json")).not.toThrow()
expect(() => assertMutablePath("/workspace/src/agent-manager.json")).not.toThrow()
})
})
@@ -159,7 +159,20 @@ describe("agent_manager tool", () => {
expect(schema.oneOf).toBeUndefined()
expect(schema.allOf).toBeUndefined()
const action = schema.properties?.action
expect(action && typeof action === "object" ? action.enum : undefined).toEqual(["list", "prompt", "stop"])
expect(action && typeof action === "object" ? action.enum : undefined).toEqual(["list", "prompt", "stop", "move"])
expect(action && typeof action === "object" ? action.description : undefined).toContain("Use list first")
expect(action && typeof action === "object" ? action.description : undefined).toContain("Never edit")
expect(schema.properties?.sessionID).toEqual(
expect.objectContaining({ description: expect.stringContaining("returned by action=list") }),
)
expect(schema.properties?.sectionID).toEqual(
expect.objectContaining({ description: expect.stringContaining("Use null to unassign") }),
)
expect(schema.properties?.sectionID).toEqual(
expect.objectContaining({
anyOf: expect.arrayContaining([expect.objectContaining({ type: "string" }), { type: "null" }]),
}),
)
expect(Object.keys(schema.properties ?? {})).toEqual([
"mode",
"versions",
@@ -168,6 +181,7 @@ describe("agent_manager tool", () => {
"filter",
"sessionID",
"prompt",
"sectionID",
])
})
@@ -226,7 +240,7 @@ describe("agent_manager tool", () => {
const result = await rt.runPromise(
provideTmpdirInstance(() =>
tool.execute(
{ action: "list" },
{ action: "list", filter: null },
{ ...ctx, ask: (input: unknown) => Effect.sync(() => permissions.push(input)) },
),
).pipe(Effect.scoped),
@@ -242,6 +256,8 @@ describe("agent_manager tool", () => {
])
expect(requests).toEqual([{ operation: "overview", sessionID: ctx.sessionID, filter: undefined }])
expect(JSON.parse(result.output)).toEqual({
instructions:
"This overview is the source of truth. Use sections[].id as sectionID and sessions[].id/session.id as sessionID for action=move. Do not edit .kilo/agent-manager.json.",
sections: [],
ungrouped: [
{
@@ -345,6 +361,58 @@ describe("agent_manager tool", () => {
await rt.dispose()
})
test("moves one existing session with a separate mutation permission pattern", async () => {
const requests: unknown[] = []
const rt = makeRuntime("test", {
request: (input) =>
Effect.sync(() => {
requests.push(input)
return {
operation: "move" as const,
sessionID: SessionID.make("ses_target"),
sectionID: "sec_review",
moved: true as const,
}
}),
})
const tool = await rt.runPromise(
Effect.gen(function* () {
return yield* Tool.init(yield* AgentManagerTool)
}),
)
const permissions: unknown[] = []
const result = await rt.runPromise(
provideTmpdirInstance(() =>
tool.execute(
{ action: "move", sessionID: SessionID.make("ses_target"), sectionID: "sec_review" },
{ ...ctx, ask: (input: unknown) => Effect.sync(() => permissions.push(input)) },
),
).pipe(Effect.scoped),
)
expect(permissions).toEqual([
{
permission: "agent_manager",
patterns: ["move"],
always: ["move"],
metadata: { action: "move", sessionID: "ses_target", sectionID: "sec_review" },
},
])
expect(requests).toEqual([
{
operation: "move",
sessionID: ctx.sessionID,
targetSessionID: "ses_target",
sectionID: "sec_review",
},
])
expect(result.output).toContain("sec_review")
expect(result.metadata).toEqual(
expect.objectContaining({ action: "move", sessionID: "ses_target", sectionID: "sec_review" }),
)
await rt.dispose()
})
test("inherits the latest invoking model and variant when omitted", async () => {
const task = await publish(runtime, { prompt: "Fix" }, [
message("msg_current", "kilo", "kilo/shared", "low", 2),
@@ -7,6 +7,7 @@ describe("Agent Manager side-effect permissions", () => {
test("requires consent despite a broad Agent Manager allow rule", () => {
expect(Permission.resolve("agent_manager", "prompt", broad).action).toBe("ask")
expect(Permission.resolve("agent_manager", "stop", broad).action).toBe("ask")
expect(Permission.resolve("agent_manager", "move", broad).action).toBe("ask")
expect(Permission.resolve("agent_manager", "local", broad).action).toBe("allow")
expect(Permission.resolve("agent_manager", "worktree", broad).action).toBe("allow")
})
@@ -15,6 +16,7 @@ describe("Agent Manager side-effect permissions", () => {
const rules = [{ permission: "*", pattern: "*", action: "allow" as const }]
expect(Permission.resolve("agent_manager", "prompt", rules).action).toBe("ask")
expect(Permission.resolve("agent_manager", "stop", rules).action).toBe("ask")
expect(Permission.resolve("agent_manager", "move", rules).action).toBe("ask")
})
test("requires consent despite a saved wildcard approval", () => {
@@ -22,11 +24,13 @@ describe("Agent Manager side-effect permissions", () => {
const saved = [{ permission: "agent_manager", pattern: "*", action: "allow" as const }]
expect(Permission.resolve("agent_manager", "prompt", rules, saved).action).toBe("ask")
expect(Permission.resolve("agent_manager", "stop", rules, saved).action).toBe("ask")
expect(Permission.resolve("agent_manager", "move", rules, saved).action).toBe("ask")
})
test("allows only explicit side-effect approvals", () => {
const rules = Permission.fromConfig({ agent_manager: { prompt: "allow", stop: "allow" } })
const rules = Permission.fromConfig({ agent_manager: { prompt: "allow", stop: "allow", move: "allow" } })
expect(Permission.resolve("agent_manager", "prompt", rules).action).toBe("allow")
expect(Permission.resolve("agent_manager", "stop", rules).action).toBe("allow")
expect(Permission.resolve("agent_manager", "move", rules).action).toBe("allow")
})
})
+26 -2
View File
@@ -281,7 +281,19 @@ export type AgentManagerStopRequest = {
targetSessionID: string
}
export type AgentManagerRequest = AgentManagerOverviewRequest | AgentManagerPromptRequest | AgentManagerStopRequest
export type AgentManagerMoveRequest = {
id: AgentManagerRequestId
sessionID: string
operation: "move"
targetSessionID: string
sectionID: string | null
}
export type AgentManagerRequest =
| AgentManagerOverviewRequest
| AgentManagerPromptRequest
| AgentManagerStopRequest
| AgentManagerMoveRequest
export type NotebookRequestId = string
@@ -3390,7 +3402,18 @@ export type AgentManagerStopResult = {
stopped: true
}
export type AgentManagerResult = AgentManagerOverviewResult | AgentManagerPromptResult | AgentManagerStopResult
export type AgentManagerMoveResult = {
operation: "move"
sessionID: string
sectionID: string | null
moved: true
}
export type AgentManagerResult =
| AgentManagerOverviewResult
| AgentManagerPromptResult
| AgentManagerStopResult
| AgentManagerMoveResult
export type AgentManagerFailure = {
code:
@@ -3401,6 +3424,7 @@ export type AgentManagerFailure = {
| "stale_session"
| "timeout"
| "unavailable_session"
| "unknown_section"
| "unknown_session"
| "workspace_unavailable"
message: string
+293 -214
View File
@@ -23784,6 +23784,13 @@
"schema": {
"type": "string"
}
},
{
"in": "query",
"name": "replayExited",
"schema": {
"type": "string"
}
}
],
"security": [],
@@ -24634,6 +24641,9 @@
{
"$ref": "#/components/schemas/EventSandboxStatusChanged"
},
{
"$ref": "#/components/schemas/EventLspClientDiagnostics"
},
{
"$ref": "#/components/schemas/EventSuggestionShown"
},
@@ -24661,9 +24671,6 @@
{
"$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed"
},
{
"$ref": "#/components/schemas/EventLspClientDiagnostics"
},
{
"$ref": "#/components/schemas/EventMemoryStatus1"
},
@@ -24679,6 +24686,9 @@
{
"$ref": "#/components/schemas/EventIndexingWarning"
},
{
"$ref": "#/components/schemas/EventModels-devRefreshed"
},
{
"$ref": "#/components/schemas/EventServerConnected"
},
@@ -24847,9 +24857,6 @@
{
"$ref": "#/components/schemas/EventSessionError"
},
{
"$ref": "#/components/schemas/EventModels-devRefreshed"
},
{
"$ref": "#/components/schemas/EventInstallationUpdated"
},
@@ -24857,13 +24864,7 @@
"$ref": "#/components/schemas/EventInstallationUpdate-available"
},
{
"$ref": "#/components/schemas/EventPermissionAsked"
},
{
"$ref": "#/components/schemas/EventPermissionReplied"
},
{
"$ref": "#/components/schemas/EventReferenceUpdated"
"$ref": "#/components/schemas/EventFileEdited"
},
{
"$ref": "#/components/schemas/EventPermissionV2Asked"
@@ -24872,10 +24873,10 @@
"$ref": "#/components/schemas/EventPermissionV2Replied"
},
{
"$ref": "#/components/schemas/EventProjectDirectoriesUpdated"
"$ref": "#/components/schemas/EventReferenceUpdated"
},
{
"$ref": "#/components/schemas/EventFileEdited"
"$ref": "#/components/schemas/EventProjectDirectoriesUpdated"
},
{
"$ref": "#/components/schemas/EventFileWatcherUpdated"
@@ -24904,6 +24905,15 @@
{
"$ref": "#/components/schemas/EventTodoUpdated"
},
{
"$ref": "#/components/schemas/EventLspUpdated"
},
{
"$ref": "#/components/schemas/EventPermissionAsked"
},
{
"$ref": "#/components/schemas/EventPermissionReplied"
},
{
"$ref": "#/components/schemas/EventSessionStatus"
},
@@ -24922,9 +24932,6 @@
{
"$ref": "#/components/schemas/EventVcsBranchUpdated"
},
{
"$ref": "#/components/schemas/EventLspUpdated"
},
{
"$ref": "#/components/schemas/EventWorkspaceReady"
},
@@ -25462,6 +25469,40 @@
"required": ["id", "sessionID", "operation", "targetSessionID"],
"additionalProperties": false
},
"AgentManagerMoveRequest": {
"type": "object",
"properties": {
"id": {
"$ref": "#/components/schemas/AgentManagerRequestID"
},
"sessionID": {
"type": "string",
"pattern": "^ses"
},
"operation": {
"type": "string",
"enum": ["move"]
},
"targetSessionID": {
"type": "string",
"pattern": "^ses"
},
"sectionID": {
"anyOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 200
},
{
"type": "null"
}
]
}
},
"required": ["id", "sessionID", "operation", "targetSessionID", "sectionID"],
"additionalProperties": false
},
"AgentManagerRequest": {
"anyOf": [
{
@@ -25472,6 +25513,9 @@
},
{
"$ref": "#/components/schemas/AgentManagerStopRequest"
},
{
"$ref": "#/components/schemas/AgentManagerMoveRequest"
}
]
},
@@ -27827,6 +27871,9 @@
{
"$ref": "#/components/schemas/EventSandboxStatusChanged"
},
{
"$ref": "#/components/schemas/EventLspClientDiagnostics"
},
{
"$ref": "#/components/schemas/EventSuggestionShown"
},
@@ -27854,9 +27901,6 @@
{
"$ref": "#/components/schemas/EventKilo-sessionsRemote-status-changed"
},
{
"$ref": "#/components/schemas/EventLspClientDiagnostics"
},
{
"$ref": "#/components/schemas/EventMemoryStatus"
},
@@ -27872,6 +27916,9 @@
{
"$ref": "#/components/schemas/EventIndexingWarning"
},
{
"$ref": "#/components/schemas/EventModels-devRefreshed"
},
{
"$ref": "#/components/schemas/EventServerConnected"
},
@@ -28040,9 +28087,6 @@
{
"$ref": "#/components/schemas/EventSessionError"
},
{
"$ref": "#/components/schemas/EventModels-devRefreshed"
},
{
"$ref": "#/components/schemas/EventInstallationUpdated"
},
@@ -28050,13 +28094,7 @@
"$ref": "#/components/schemas/EventInstallationUpdate-available"
},
{
"$ref": "#/components/schemas/EventPermissionAsked"
},
{
"$ref": "#/components/schemas/EventPermissionReplied"
},
{
"$ref": "#/components/schemas/EventReferenceUpdated"
"$ref": "#/components/schemas/EventFileEdited"
},
{
"$ref": "#/components/schemas/EventPermissionV2Asked"
@@ -28065,10 +28103,10 @@
"$ref": "#/components/schemas/EventPermissionV2Replied"
},
{
"$ref": "#/components/schemas/EventProjectDirectoriesUpdated"
"$ref": "#/components/schemas/EventReferenceUpdated"
},
{
"$ref": "#/components/schemas/EventFileEdited"
"$ref": "#/components/schemas/EventProjectDirectoriesUpdated"
},
{
"$ref": "#/components/schemas/EventFileWatcherUpdated"
@@ -28097,6 +28135,15 @@
{
"$ref": "#/components/schemas/EventTodoUpdated"
},
{
"$ref": "#/components/schemas/EventLspUpdated"
},
{
"$ref": "#/components/schemas/EventPermissionAsked"
},
{
"$ref": "#/components/schemas/EventPermissionReplied"
},
{
"$ref": "#/components/schemas/EventSessionStatus"
},
@@ -28115,9 +28162,6 @@
{
"$ref": "#/components/schemas/EventVcsBranchUpdated"
},
{
"$ref": "#/components/schemas/EventLspUpdated"
},
{
"$ref": "#/components/schemas/EventWorkspaceReady"
},
@@ -34810,6 +34854,37 @@
"required": ["operation", "sessionID", "stopped"],
"additionalProperties": false
},
"AgentManagerMoveResult": {
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["move"]
},
"sessionID": {
"type": "string",
"pattern": "^ses"
},
"sectionID": {
"anyOf": [
{
"type": "string",
"minLength": 1,
"maxLength": 200
},
{
"type": "null"
}
]
},
"moved": {
"type": "boolean",
"enum": [true]
}
},
"required": ["operation", "sessionID", "sectionID", "moved"],
"additionalProperties": false
},
"AgentManagerResult": {
"anyOf": [
{
@@ -34820,6 +34895,9 @@
},
{
"$ref": "#/components/schemas/AgentManagerStopResult"
},
{
"$ref": "#/components/schemas/AgentManagerMoveResult"
}
]
},
@@ -34836,6 +34914,7 @@
"stale_session",
"timeout",
"unavailable_session",
"unknown_section",
"unknown_session",
"workspace_unavailable"
]
@@ -35821,6 +35900,33 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventLspClientDiagnostics": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["lsp.client.diagnostics"]
},
"properties": {
"type": "object",
"properties": {
"serverID": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": ["serverID", "path"],
"additionalProperties": false
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventSuggestionShown": {
"type": "object",
"properties": {
@@ -36117,33 +36223,6 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventLspClientDiagnostics": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["lsp.client.diagnostics"]
},
"properties": {
"type": "object",
"properties": {
"serverID": {
"type": "string"
},
"path": {
"type": "string"
}
},
"required": ["serverID", "path"],
"additionalProperties": false
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventMemoryStatus": {
"type": "object",
"properties": {
@@ -37214,6 +37293,24 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventModels-devRefreshed": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["models-dev.refreshed"]
},
"properties": {
"type": "object",
"properties": {}
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventServerConnected": {
"type": "object",
"properties": {
@@ -39256,24 +39353,6 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventModels-devRefreshed": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["models-dev.refreshed"]
},
"properties": {
"type": "object",
"properties": {}
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventInstallationUpdated": {
"type": "object",
"properties": {
@@ -39322,7 +39401,7 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventPermissionAsked": {
"EventFileEdited": {
"type": "object",
"properties": {
"id": {
@@ -39330,109 +39409,22 @@
},
"type": {
"type": "string",
"enum": ["permission.asked"]
"enum": ["file.edited"]
},
"properties": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^per"
},
"sessionID": {
"type": "string",
"pattern": "^ses"
},
"permission": {
"file": {
"type": "string"
},
"patterns": {
"type": "array",
"items": {
"type": "string"
}
},
"metadata": {
"type": "object"
},
"always": {
"type": "array",
"items": {
"type": "string"
}
},
"tool": {
"type": "object",
"properties": {
"messageID": {
"type": "string"
},
"callID": {
"type": "string"
}
},
"required": ["messageID", "callID"],
"additionalProperties": false
}
},
"required": ["id", "sessionID", "permission", "patterns", "metadata", "always"],
"required": ["file"],
"additionalProperties": false
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventPermissionReplied": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["permission.replied"]
},
"properties": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"pattern": "^ses"
},
"requestID": {
"type": "string",
"pattern": "^per"
},
"reply": {
"type": "string",
"enum": ["once", "always", "reject"]
}
},
"required": ["sessionID", "requestID", "reply"],
"additionalProperties": false
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventReferenceUpdated": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["reference.updated"]
},
"properties": {
"type": "object",
"properties": {}
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"PermissionV2Source": {
"type": "object",
"properties": {
@@ -39536,6 +39528,24 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventReferenceUpdated": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["reference.updated"]
},
"properties": {
"type": "object",
"properties": {}
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventProjectDirectoriesUpdated": {
"type": "object",
"properties": {
@@ -39560,30 +39570,6 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventFileEdited": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["file.edited"]
},
"properties": {
"type": "object",
"properties": {
"file": {
"type": "string"
}
},
"required": ["file"],
"additionalProperties": false
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventFileWatcherUpdated": {
"type": "object",
"properties": {
@@ -39910,6 +39896,117 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventLspUpdated": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["lsp.updated"]
},
"properties": {
"type": "object",
"properties": {}
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventPermissionAsked": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["permission.asked"]
},
"properties": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^per"
},
"sessionID": {
"type": "string",
"pattern": "^ses"
},
"permission": {
"type": "string"
},
"patterns": {
"type": "array",
"items": {
"type": "string"
}
},
"metadata": {
"type": "object"
},
"always": {
"type": "array",
"items": {
"type": "string"
}
},
"tool": {
"type": "object",
"properties": {
"messageID": {
"type": "string"
},
"callID": {
"type": "string"
}
},
"required": ["messageID", "callID"],
"additionalProperties": false
}
},
"required": ["id", "sessionID", "permission", "patterns", "metadata", "always"],
"additionalProperties": false
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventPermissionReplied": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["permission.replied"]
},
"properties": {
"type": "object",
"properties": {
"sessionID": {
"type": "string",
"pattern": "^ses"
},
"requestID": {
"type": "string",
"pattern": "^per"
},
"reply": {
"type": "string",
"enum": ["once", "always", "reject"]
}
},
"required": ["sessionID", "requestID", "reply"],
"additionalProperties": false
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventSessionStatus": {
"type": "object",
"properties": {
@@ -40130,24 +40227,6 @@
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventLspUpdated": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["lsp.updated"]
},
"properties": {
"type": "object",
"properties": {}
}
},
"required": ["id", "type", "properties"],
"additionalProperties": false
},
"EventWorkspaceReady": {
"type": "object",
"properties": {