feat(gateway): add agents.create/update/delete methods (#11045)
* feat(gateway): add agents.create/update/delete methods * fix(lint): preserve memory-lancedb load error cause * feat(gateway): trash agent files on agents.delete * chore(protocol): regenerate Swift gateway models * fix(gateway): stabilize agents.create dirs and agentDir * feat(gateway): support avatar in agents.create * fix: prep agents.create/update/delete handlers (#11045) (thanks @advaitpaliwal) - Reuse movePathToTrash from browser/trash.ts (has ~/.Trash fallback on non-macOS) - Fix partial-failure: workspace setup now runs before config write - Always write Name to IDENTITY.md regardless of emoji/avatar - Add unit tests for agents.create, agents.update, agents.delete - Add CHANGELOG entry --------- Co-authored-by: Tyler Yust <TYTYYUST@YAHOO.COM>
This commit is contained in:
@@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Gateway: add `agents.create`, `agents.update`, `agents.delete` RPC methods for web UI agent management. (#11045) Thanks @advaitpaliwal.
|
||||||
- Cron: run history deep-links to session chat from the dashboard. (#10776) Thanks @tyler6204.
|
- Cron: run history deep-links to session chat from the dashboard. (#10776) Thanks @tyler6204.
|
||||||
- Cron: per-run session keys in run log entries and default labels for cron sessions. (#10776) Thanks @tyler6204.
|
- Cron: per-run session keys in run log entries and default labels for cron sessions. (#10776) Thanks @tyler6204.
|
||||||
- Cron: legacy payload field compatibility (`deliver`, `channel`, `to`, `bestEffortDeliver`) in schema. (#10776) Thanks @tyler6204.
|
- Cron: legacy payload field compatibility (`deliver`, `channel`, `to`, `bestEffortDeliver`) in schema. (#10776) Thanks @tyler6204.
|
||||||
|
|||||||
@@ -1589,6 +1589,140 @@ public struct AgentSummary: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct AgentsCreateParams: Codable, Sendable {
|
||||||
|
public let name: String
|
||||||
|
public let workspace: String
|
||||||
|
public let emoji: String?
|
||||||
|
public let avatar: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
name: String,
|
||||||
|
workspace: String,
|
||||||
|
emoji: String?,
|
||||||
|
avatar: String?
|
||||||
|
) {
|
||||||
|
self.name = name
|
||||||
|
self.workspace = workspace
|
||||||
|
self.emoji = emoji
|
||||||
|
self.avatar = avatar
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case name
|
||||||
|
case workspace
|
||||||
|
case emoji
|
||||||
|
case avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsCreateResult: Codable, Sendable {
|
||||||
|
public let ok: Bool
|
||||||
|
public let agentid: String
|
||||||
|
public let name: String
|
||||||
|
public let workspace: String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
ok: Bool,
|
||||||
|
agentid: String,
|
||||||
|
name: String,
|
||||||
|
workspace: String
|
||||||
|
) {
|
||||||
|
self.ok = ok
|
||||||
|
self.agentid = agentid
|
||||||
|
self.name = name
|
||||||
|
self.workspace = workspace
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case ok
|
||||||
|
case agentid = "agentId"
|
||||||
|
case name
|
||||||
|
case workspace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsUpdateParams: Codable, Sendable {
|
||||||
|
public let agentid: String
|
||||||
|
public let name: String?
|
||||||
|
public let workspace: String?
|
||||||
|
public let model: String?
|
||||||
|
public let avatar: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
agentid: String,
|
||||||
|
name: String?,
|
||||||
|
workspace: String?,
|
||||||
|
model: String?,
|
||||||
|
avatar: String?
|
||||||
|
) {
|
||||||
|
self.agentid = agentid
|
||||||
|
self.name = name
|
||||||
|
self.workspace = workspace
|
||||||
|
self.model = model
|
||||||
|
self.avatar = avatar
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case agentid = "agentId"
|
||||||
|
case name
|
||||||
|
case workspace
|
||||||
|
case model
|
||||||
|
case avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsUpdateResult: Codable, Sendable {
|
||||||
|
public let ok: Bool
|
||||||
|
public let agentid: String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
ok: Bool,
|
||||||
|
agentid: String
|
||||||
|
) {
|
||||||
|
self.ok = ok
|
||||||
|
self.agentid = agentid
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case ok
|
||||||
|
case agentid = "agentId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsDeleteParams: Codable, Sendable {
|
||||||
|
public let agentid: String
|
||||||
|
public let deletefiles: Bool?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
agentid: String,
|
||||||
|
deletefiles: Bool?
|
||||||
|
) {
|
||||||
|
self.agentid = agentid
|
||||||
|
self.deletefiles = deletefiles
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case agentid = "agentId"
|
||||||
|
case deletefiles = "deleteFiles"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsDeleteResult: Codable, Sendable {
|
||||||
|
public let ok: Bool
|
||||||
|
public let agentid: String
|
||||||
|
public let removedbindings: Int
|
||||||
|
|
||||||
|
public init(
|
||||||
|
ok: Bool,
|
||||||
|
agentid: String,
|
||||||
|
removedbindings: Int
|
||||||
|
) {
|
||||||
|
self.ok = ok
|
||||||
|
self.agentid = agentid
|
||||||
|
self.removedbindings = removedbindings
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case ok
|
||||||
|
case agentid = "agentId"
|
||||||
|
case removedbindings = "removedBindings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct AgentsFileEntry: Codable, Sendable {
|
public struct AgentsFileEntry: Codable, Sendable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let path: String
|
public let path: String
|
||||||
|
|||||||
@@ -1589,6 +1589,140 @@ public struct AgentSummary: Codable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct AgentsCreateParams: Codable, Sendable {
|
||||||
|
public let name: String
|
||||||
|
public let workspace: String
|
||||||
|
public let emoji: String?
|
||||||
|
public let avatar: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
name: String,
|
||||||
|
workspace: String,
|
||||||
|
emoji: String?,
|
||||||
|
avatar: String?
|
||||||
|
) {
|
||||||
|
self.name = name
|
||||||
|
self.workspace = workspace
|
||||||
|
self.emoji = emoji
|
||||||
|
self.avatar = avatar
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case name
|
||||||
|
case workspace
|
||||||
|
case emoji
|
||||||
|
case avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsCreateResult: Codable, Sendable {
|
||||||
|
public let ok: Bool
|
||||||
|
public let agentid: String
|
||||||
|
public let name: String
|
||||||
|
public let workspace: String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
ok: Bool,
|
||||||
|
agentid: String,
|
||||||
|
name: String,
|
||||||
|
workspace: String
|
||||||
|
) {
|
||||||
|
self.ok = ok
|
||||||
|
self.agentid = agentid
|
||||||
|
self.name = name
|
||||||
|
self.workspace = workspace
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case ok
|
||||||
|
case agentid = "agentId"
|
||||||
|
case name
|
||||||
|
case workspace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsUpdateParams: Codable, Sendable {
|
||||||
|
public let agentid: String
|
||||||
|
public let name: String?
|
||||||
|
public let workspace: String?
|
||||||
|
public let model: String?
|
||||||
|
public let avatar: String?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
agentid: String,
|
||||||
|
name: String?,
|
||||||
|
workspace: String?,
|
||||||
|
model: String?,
|
||||||
|
avatar: String?
|
||||||
|
) {
|
||||||
|
self.agentid = agentid
|
||||||
|
self.name = name
|
||||||
|
self.workspace = workspace
|
||||||
|
self.model = model
|
||||||
|
self.avatar = avatar
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case agentid = "agentId"
|
||||||
|
case name
|
||||||
|
case workspace
|
||||||
|
case model
|
||||||
|
case avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsUpdateResult: Codable, Sendable {
|
||||||
|
public let ok: Bool
|
||||||
|
public let agentid: String
|
||||||
|
|
||||||
|
public init(
|
||||||
|
ok: Bool,
|
||||||
|
agentid: String
|
||||||
|
) {
|
||||||
|
self.ok = ok
|
||||||
|
self.agentid = agentid
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case ok
|
||||||
|
case agentid = "agentId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsDeleteParams: Codable, Sendable {
|
||||||
|
public let agentid: String
|
||||||
|
public let deletefiles: Bool?
|
||||||
|
|
||||||
|
public init(
|
||||||
|
agentid: String,
|
||||||
|
deletefiles: Bool?
|
||||||
|
) {
|
||||||
|
self.agentid = agentid
|
||||||
|
self.deletefiles = deletefiles
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case agentid = "agentId"
|
||||||
|
case deletefiles = "deleteFiles"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AgentsDeleteResult: Codable, Sendable {
|
||||||
|
public let ok: Bool
|
||||||
|
public let agentid: String
|
||||||
|
public let removedbindings: Int
|
||||||
|
|
||||||
|
public init(
|
||||||
|
ok: Bool,
|
||||||
|
agentid: String,
|
||||||
|
removedbindings: Int
|
||||||
|
) {
|
||||||
|
self.ok = ok
|
||||||
|
self.agentid = agentid
|
||||||
|
self.removedbindings = removedbindings
|
||||||
|
}
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case ok
|
||||||
|
case agentid = "agentId"
|
||||||
|
case removedbindings = "removedBindings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct AgentsFileEntry: Codable, Sendable {
|
public struct AgentsFileEntry: Codable, Sendable {
|
||||||
public let name: String
|
public let name: String
|
||||||
public let path: String
|
public let path: String
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const loadLanceDB = async (): Promise<typeof import("@lancedb/lancedb")> => {
|
|||||||
return await lancedbImportPromise;
|
return await lancedbImportPromise;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Common on macOS today: upstream package may not ship darwin native bindings.
|
// Common on macOS today: upstream package may not ship darwin native bindings.
|
||||||
throw new Error(`memory-lancedb: failed to load LanceDB. ${String(err)}`);
|
throw new Error(`memory-lancedb: failed to load LanceDB. ${String(err)}`, { cause: err });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,18 @@ import {
|
|||||||
AgentSummarySchema,
|
AgentSummarySchema,
|
||||||
type AgentsFileEntry,
|
type AgentsFileEntry,
|
||||||
AgentsFileEntrySchema,
|
AgentsFileEntrySchema,
|
||||||
|
type AgentsCreateParams,
|
||||||
|
AgentsCreateParamsSchema,
|
||||||
|
type AgentsCreateResult,
|
||||||
|
AgentsCreateResultSchema,
|
||||||
|
type AgentsUpdateParams,
|
||||||
|
AgentsUpdateParamsSchema,
|
||||||
|
type AgentsUpdateResult,
|
||||||
|
AgentsUpdateResultSchema,
|
||||||
|
type AgentsDeleteParams,
|
||||||
|
AgentsDeleteParamsSchema,
|
||||||
|
type AgentsDeleteResult,
|
||||||
|
AgentsDeleteResultSchema,
|
||||||
type AgentsFilesGetParams,
|
type AgentsFilesGetParams,
|
||||||
AgentsFilesGetParamsSchema,
|
AgentsFilesGetParamsSchema,
|
||||||
type AgentsFilesGetResult,
|
type AgentsFilesGetResult,
|
||||||
@@ -226,6 +238,9 @@ export const validateAgentIdentityParams =
|
|||||||
export const validateAgentWaitParams = ajv.compile<AgentWaitParams>(AgentWaitParamsSchema);
|
export const validateAgentWaitParams = ajv.compile<AgentWaitParams>(AgentWaitParamsSchema);
|
||||||
export const validateWakeParams = ajv.compile<WakeParams>(WakeParamsSchema);
|
export const validateWakeParams = ajv.compile<WakeParams>(WakeParamsSchema);
|
||||||
export const validateAgentsListParams = ajv.compile<AgentsListParams>(AgentsListParamsSchema);
|
export const validateAgentsListParams = ajv.compile<AgentsListParams>(AgentsListParamsSchema);
|
||||||
|
export const validateAgentsCreateParams = ajv.compile<AgentsCreateParams>(AgentsCreateParamsSchema);
|
||||||
|
export const validateAgentsUpdateParams = ajv.compile<AgentsUpdateParams>(AgentsUpdateParamsSchema);
|
||||||
|
export const validateAgentsDeleteParams = ajv.compile<AgentsDeleteParams>(AgentsDeleteParamsSchema);
|
||||||
export const validateAgentsFilesListParams = ajv.compile<AgentsFilesListParams>(
|
export const validateAgentsFilesListParams = ajv.compile<AgentsFilesListParams>(
|
||||||
AgentsFilesListParamsSchema,
|
AgentsFilesListParamsSchema,
|
||||||
);
|
);
|
||||||
@@ -438,6 +453,12 @@ export {
|
|||||||
WebLoginWaitParamsSchema,
|
WebLoginWaitParamsSchema,
|
||||||
AgentSummarySchema,
|
AgentSummarySchema,
|
||||||
AgentsFileEntrySchema,
|
AgentsFileEntrySchema,
|
||||||
|
AgentsCreateParamsSchema,
|
||||||
|
AgentsCreateResultSchema,
|
||||||
|
AgentsUpdateParamsSchema,
|
||||||
|
AgentsUpdateResultSchema,
|
||||||
|
AgentsDeleteParamsSchema,
|
||||||
|
AgentsDeleteResultSchema,
|
||||||
AgentsFilesListParamsSchema,
|
AgentsFilesListParamsSchema,
|
||||||
AgentsFilesListResultSchema,
|
AgentsFilesListResultSchema,
|
||||||
AgentsFilesGetParamsSchema,
|
AgentsFilesGetParamsSchema,
|
||||||
@@ -519,6 +540,12 @@ export type {
|
|||||||
WebLoginWaitParams,
|
WebLoginWaitParams,
|
||||||
AgentSummary,
|
AgentSummary,
|
||||||
AgentsFileEntry,
|
AgentsFileEntry,
|
||||||
|
AgentsCreateParams,
|
||||||
|
AgentsCreateResult,
|
||||||
|
AgentsUpdateParams,
|
||||||
|
AgentsUpdateResult,
|
||||||
|
AgentsDeleteParams,
|
||||||
|
AgentsDeleteResult,
|
||||||
AgentsFilesListParams,
|
AgentsFilesListParams,
|
||||||
AgentsFilesListResult,
|
AgentsFilesListResult,
|
||||||
AgentsFilesGetParams,
|
AgentsFilesGetParams,
|
||||||
|
|||||||
@@ -44,6 +44,62 @@ export const AgentsListResultSchema = Type.Object(
|
|||||||
{ additionalProperties: false },
|
{ additionalProperties: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const AgentsCreateParamsSchema = Type.Object(
|
||||||
|
{
|
||||||
|
name: NonEmptyString,
|
||||||
|
workspace: NonEmptyString,
|
||||||
|
emoji: Type.Optional(Type.String()),
|
||||||
|
avatar: Type.Optional(Type.String()),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentsCreateResultSchema = Type.Object(
|
||||||
|
{
|
||||||
|
ok: Type.Literal(true),
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
name: NonEmptyString,
|
||||||
|
workspace: NonEmptyString,
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentsUpdateParamsSchema = Type.Object(
|
||||||
|
{
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
name: Type.Optional(NonEmptyString),
|
||||||
|
workspace: Type.Optional(NonEmptyString),
|
||||||
|
model: Type.Optional(NonEmptyString),
|
||||||
|
avatar: Type.Optional(Type.String()),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentsUpdateResultSchema = Type.Object(
|
||||||
|
{
|
||||||
|
ok: Type.Literal(true),
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentsDeleteParamsSchema = Type.Object(
|
||||||
|
{
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
deleteFiles: Type.Optional(Type.Boolean()),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AgentsDeleteResultSchema = Type.Object(
|
||||||
|
{
|
||||||
|
ok: Type.Literal(true),
|
||||||
|
agentId: NonEmptyString,
|
||||||
|
removedBindings: Type.Integer({ minimum: 0 }),
|
||||||
|
},
|
||||||
|
{ additionalProperties: false },
|
||||||
|
);
|
||||||
|
|
||||||
export const AgentsFileEntrySchema = Type.Object(
|
export const AgentsFileEntrySchema = Type.Object(
|
||||||
{
|
{
|
||||||
name: NonEmptyString,
|
name: NonEmptyString,
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import {
|
|||||||
} from "./agent.js";
|
} from "./agent.js";
|
||||||
import {
|
import {
|
||||||
AgentSummarySchema,
|
AgentSummarySchema,
|
||||||
|
AgentsCreateParamsSchema,
|
||||||
|
AgentsCreateResultSchema,
|
||||||
|
AgentsDeleteParamsSchema,
|
||||||
|
AgentsDeleteResultSchema,
|
||||||
AgentsFileEntrySchema,
|
AgentsFileEntrySchema,
|
||||||
AgentsFilesGetParamsSchema,
|
AgentsFilesGetParamsSchema,
|
||||||
AgentsFilesGetResultSchema,
|
AgentsFilesGetResultSchema,
|
||||||
@@ -20,6 +24,8 @@ import {
|
|||||||
AgentsFilesSetResultSchema,
|
AgentsFilesSetResultSchema,
|
||||||
AgentsListParamsSchema,
|
AgentsListParamsSchema,
|
||||||
AgentsListResultSchema,
|
AgentsListResultSchema,
|
||||||
|
AgentsUpdateParamsSchema,
|
||||||
|
AgentsUpdateResultSchema,
|
||||||
ModelChoiceSchema,
|
ModelChoiceSchema,
|
||||||
ModelsListParamsSchema,
|
ModelsListParamsSchema,
|
||||||
ModelsListResultSchema,
|
ModelsListResultSchema,
|
||||||
@@ -191,6 +197,12 @@ export const ProtocolSchemas: Record<string, TSchema> = {
|
|||||||
WebLoginStartParams: WebLoginStartParamsSchema,
|
WebLoginStartParams: WebLoginStartParamsSchema,
|
||||||
WebLoginWaitParams: WebLoginWaitParamsSchema,
|
WebLoginWaitParams: WebLoginWaitParamsSchema,
|
||||||
AgentSummary: AgentSummarySchema,
|
AgentSummary: AgentSummarySchema,
|
||||||
|
AgentsCreateParams: AgentsCreateParamsSchema,
|
||||||
|
AgentsCreateResult: AgentsCreateResultSchema,
|
||||||
|
AgentsUpdateParams: AgentsUpdateParamsSchema,
|
||||||
|
AgentsUpdateResult: AgentsUpdateResultSchema,
|
||||||
|
AgentsDeleteParams: AgentsDeleteParamsSchema,
|
||||||
|
AgentsDeleteResult: AgentsDeleteResultSchema,
|
||||||
AgentsFileEntry: AgentsFileEntrySchema,
|
AgentsFileEntry: AgentsFileEntrySchema,
|
||||||
AgentsFilesListParams: AgentsFilesListParamsSchema,
|
AgentsFilesListParams: AgentsFilesListParamsSchema,
|
||||||
AgentsFilesListResult: AgentsFilesListResultSchema,
|
AgentsFilesListResult: AgentsFilesListResultSchema,
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import type {
|
|||||||
import type {
|
import type {
|
||||||
AgentSummarySchema,
|
AgentSummarySchema,
|
||||||
AgentsFileEntrySchema,
|
AgentsFileEntrySchema,
|
||||||
|
AgentsCreateParamsSchema,
|
||||||
|
AgentsCreateResultSchema,
|
||||||
|
AgentsDeleteParamsSchema,
|
||||||
|
AgentsDeleteResultSchema,
|
||||||
AgentsFilesGetParamsSchema,
|
AgentsFilesGetParamsSchema,
|
||||||
AgentsFilesGetResultSchema,
|
AgentsFilesGetResultSchema,
|
||||||
AgentsFilesListParamsSchema,
|
AgentsFilesListParamsSchema,
|
||||||
@@ -18,6 +22,8 @@ import type {
|
|||||||
AgentsFilesSetResultSchema,
|
AgentsFilesSetResultSchema,
|
||||||
AgentsListParamsSchema,
|
AgentsListParamsSchema,
|
||||||
AgentsListResultSchema,
|
AgentsListResultSchema,
|
||||||
|
AgentsUpdateParamsSchema,
|
||||||
|
AgentsUpdateResultSchema,
|
||||||
ModelChoiceSchema,
|
ModelChoiceSchema,
|
||||||
ModelsListParamsSchema,
|
ModelsListParamsSchema,
|
||||||
ModelsListResultSchema,
|
ModelsListResultSchema,
|
||||||
@@ -181,6 +187,12 @@ export type WebLoginStartParams = Static<typeof WebLoginStartParamsSchema>;
|
|||||||
export type WebLoginWaitParams = Static<typeof WebLoginWaitParamsSchema>;
|
export type WebLoginWaitParams = Static<typeof WebLoginWaitParamsSchema>;
|
||||||
export type AgentSummary = Static<typeof AgentSummarySchema>;
|
export type AgentSummary = Static<typeof AgentSummarySchema>;
|
||||||
export type AgentsFileEntry = Static<typeof AgentsFileEntrySchema>;
|
export type AgentsFileEntry = Static<typeof AgentsFileEntrySchema>;
|
||||||
|
export type AgentsCreateParams = Static<typeof AgentsCreateParamsSchema>;
|
||||||
|
export type AgentsCreateResult = Static<typeof AgentsCreateResultSchema>;
|
||||||
|
export type AgentsUpdateParams = Static<typeof AgentsUpdateParamsSchema>;
|
||||||
|
export type AgentsUpdateResult = Static<typeof AgentsUpdateResultSchema>;
|
||||||
|
export type AgentsDeleteParams = Static<typeof AgentsDeleteParamsSchema>;
|
||||||
|
export type AgentsDeleteResult = Static<typeof AgentsDeleteResultSchema>;
|
||||||
export type AgentsFilesListParams = Static<typeof AgentsFilesListParamsSchema>;
|
export type AgentsFilesListParams = Static<typeof AgentsFilesListParamsSchema>;
|
||||||
export type AgentsFilesListResult = Static<typeof AgentsFilesListResultSchema>;
|
export type AgentsFilesListResult = Static<typeof AgentsFilesListResultSchema>;
|
||||||
export type AgentsFilesGetParams = Static<typeof AgentsFilesGetParamsSchema>;
|
export type AgentsFilesGetParams = Static<typeof AgentsFilesGetParamsSchema>;
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ const BASE_METHODS = [
|
|||||||
"talk.mode",
|
"talk.mode",
|
||||||
"models.list",
|
"models.list",
|
||||||
"agents.list",
|
"agents.list",
|
||||||
|
"agents.create",
|
||||||
|
"agents.update",
|
||||||
|
"agents.delete",
|
||||||
"agents.files.list",
|
"agents.files.list",
|
||||||
"agents.files.get",
|
"agents.files.get",
|
||||||
"agents.files.set",
|
"agents.files.set",
|
||||||
|
|||||||
@@ -143,6 +143,9 @@ function authorizeGatewayMethod(method: string, client: GatewayRequestOptions["c
|
|||||||
method.startsWith("wizard.") ||
|
method.startsWith("wizard.") ||
|
||||||
method.startsWith("update.") ||
|
method.startsWith("update.") ||
|
||||||
method === "channels.logout" ||
|
method === "channels.logout" ||
|
||||||
|
method === "agents.create" ||
|
||||||
|
method === "agents.update" ||
|
||||||
|
method === "agents.delete" ||
|
||||||
method === "skills.install" ||
|
method === "skills.install" ||
|
||||||
method === "skills.update" ||
|
method === "skills.update" ||
|
||||||
method === "cron.add" ||
|
method === "cron.add" ||
|
||||||
|
|||||||
373
src/gateway/server-methods/agents-mutate.test.ts
Normal file
373
src/gateway/server-methods/agents-mutate.test.ts
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Mocks */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
loadConfigReturn: {} as Record<string, unknown>,
|
||||||
|
listAgentEntries: vi.fn(() => [] as Array<{ agentId: string }>),
|
||||||
|
findAgentEntryIndex: vi.fn(() => -1),
|
||||||
|
applyAgentConfig: vi.fn((_cfg: unknown, _opts: unknown) => ({})),
|
||||||
|
pruneAgentConfig: vi.fn(() => ({ config: {}, removedBindings: 0 })),
|
||||||
|
writeConfigFile: vi.fn(async () => {}),
|
||||||
|
ensureAgentWorkspace: vi.fn(async () => {}),
|
||||||
|
resolveAgentDir: vi.fn(() => "/agents/test-agent"),
|
||||||
|
resolveAgentWorkspaceDir: vi.fn(() => "/workspace/test-agent"),
|
||||||
|
resolveSessionTranscriptsDirForAgent: vi.fn(() => "/transcripts/test-agent"),
|
||||||
|
listAgentsForGateway: vi.fn(() => ({
|
||||||
|
defaultId: "main",
|
||||||
|
mainKey: "agent:main:main",
|
||||||
|
scope: "global",
|
||||||
|
agents: [],
|
||||||
|
})),
|
||||||
|
movePathToTrash: vi.fn(async () => "/trashed"),
|
||||||
|
fsAccess: vi.fn(async () => {}),
|
||||||
|
fsMkdir: vi.fn(async () => undefined),
|
||||||
|
fsAppendFile: vi.fn(async () => {}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../config/config.js", () => ({
|
||||||
|
loadConfig: () => mocks.loadConfigReturn,
|
||||||
|
writeConfigFile: mocks.writeConfigFile,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../commands/agents.config.js", () => ({
|
||||||
|
applyAgentConfig: mocks.applyAgentConfig,
|
||||||
|
findAgentEntryIndex: mocks.findAgentEntryIndex,
|
||||||
|
listAgentEntries: mocks.listAgentEntries,
|
||||||
|
pruneAgentConfig: mocks.pruneAgentConfig,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../agents/agent-scope.js", () => ({
|
||||||
|
listAgentIds: () => ["main"],
|
||||||
|
resolveAgentDir: mocks.resolveAgentDir,
|
||||||
|
resolveAgentWorkspaceDir: mocks.resolveAgentWorkspaceDir,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../agents/workspace.js", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("../../agents/workspace.js")>(
|
||||||
|
"../../agents/workspace.js",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
ensureAgentWorkspace: mocks.ensureAgentWorkspace,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("../../config/sessions/paths.js", () => ({
|
||||||
|
resolveSessionTranscriptsDirForAgent: mocks.resolveSessionTranscriptsDirForAgent,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../browser/trash.js", () => ({
|
||||||
|
movePathToTrash: mocks.movePathToTrash,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../../utils.js", () => ({
|
||||||
|
resolveUserPath: (p: string) => `/resolved${p.startsWith("/") ? "" : "/"}${p}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../session-utils.js", () => ({
|
||||||
|
listAgentsForGateway: mocks.listAgentsForGateway,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock node:fs/promises – agents.ts uses `import fs from "node:fs/promises"`
|
||||||
|
// which resolves to the module namespace default, so we spread actual and
|
||||||
|
// override the methods we need, plus set `default` explicitly.
|
||||||
|
vi.mock("node:fs/promises", async () => {
|
||||||
|
const actual = await vi.importActual<typeof import("node:fs/promises")>("node:fs/promises");
|
||||||
|
const patched = {
|
||||||
|
...actual,
|
||||||
|
access: mocks.fsAccess,
|
||||||
|
mkdir: mocks.fsMkdir,
|
||||||
|
appendFile: mocks.fsAppendFile,
|
||||||
|
};
|
||||||
|
return { ...patched, default: patched };
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Import after mocks are set up */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
const { agentsHandlers } = await import("./agents.js");
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Helpers */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
function makeCall(method: keyof typeof agentsHandlers, params: Record<string, unknown>) {
|
||||||
|
const respond = vi.fn();
|
||||||
|
const handler = agentsHandlers[method];
|
||||||
|
const promise = handler({
|
||||||
|
params,
|
||||||
|
respond,
|
||||||
|
context: {} as never,
|
||||||
|
req: { type: "req" as const, id: "1", method },
|
||||||
|
client: null,
|
||||||
|
isWebchatConnect: () => false,
|
||||||
|
});
|
||||||
|
return { respond, promise };
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Tests */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
describe("agents.create", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mocks.loadConfigReturn = {};
|
||||||
|
mocks.findAgentEntryIndex.mockReturnValue(-1);
|
||||||
|
mocks.applyAgentConfig.mockImplementation((_cfg, _opts) => ({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a new agent successfully", async () => {
|
||||||
|
const { respond, promise } = makeCall("agents.create", {
|
||||||
|
name: "Test Agent",
|
||||||
|
workspace: "/home/user/agents/test",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledWith(
|
||||||
|
true,
|
||||||
|
expect.objectContaining({
|
||||||
|
ok: true,
|
||||||
|
agentId: "test-agent",
|
||||||
|
name: "Test Agent",
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(mocks.ensureAgentWorkspace).toHaveBeenCalled();
|
||||||
|
expect(mocks.writeConfigFile).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ensures workspace is set up before writing config", async () => {
|
||||||
|
const callOrder: string[] = [];
|
||||||
|
mocks.ensureAgentWorkspace.mockImplementation(async () => {
|
||||||
|
callOrder.push("ensureAgentWorkspace");
|
||||||
|
});
|
||||||
|
mocks.writeConfigFile.mockImplementation(async () => {
|
||||||
|
callOrder.push("writeConfigFile");
|
||||||
|
});
|
||||||
|
|
||||||
|
const { promise } = makeCall("agents.create", {
|
||||||
|
name: "Order Test",
|
||||||
|
workspace: "/tmp/ws",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(callOrder.indexOf("ensureAgentWorkspace")).toBeLessThan(
|
||||||
|
callOrder.indexOf("writeConfigFile"),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects creating an agent with reserved 'main' id", async () => {
|
||||||
|
const { respond, promise } = makeCall("agents.create", {
|
||||||
|
name: "main",
|
||||||
|
workspace: "/tmp/ws",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledWith(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({ message: expect.stringContaining("reserved") }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects creating a duplicate agent", async () => {
|
||||||
|
mocks.findAgentEntryIndex.mockReturnValue(0);
|
||||||
|
|
||||||
|
const { respond, promise } = makeCall("agents.create", {
|
||||||
|
name: "Existing",
|
||||||
|
workspace: "/tmp/ws",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledWith(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({ message: expect.stringContaining("already exists") }),
|
||||||
|
);
|
||||||
|
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid params (missing name)", async () => {
|
||||||
|
const { respond, promise } = makeCall("agents.create", {
|
||||||
|
workspace: "/tmp/ws",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledWith(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({ message: expect.stringContaining("invalid") }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("always writes Name to IDENTITY.md even without emoji/avatar", async () => {
|
||||||
|
const { promise } = makeCall("agents.create", {
|
||||||
|
name: "Plain Agent",
|
||||||
|
workspace: "/tmp/ws",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(mocks.fsAppendFile).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("IDENTITY.md"),
|
||||||
|
expect.stringContaining("- Name: Plain Agent"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("writes emoji and avatar to IDENTITY.md when provided", async () => {
|
||||||
|
const { promise } = makeCall("agents.create", {
|
||||||
|
name: "Fancy Agent",
|
||||||
|
workspace: "/tmp/ws",
|
||||||
|
emoji: "🤖",
|
||||||
|
avatar: "https://example.com/avatar.png",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(mocks.fsAppendFile).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("IDENTITY.md"),
|
||||||
|
expect.stringMatching(/- Name: Fancy Agent[\s\S]*- Emoji: 🤖[\s\S]*- Avatar:/),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("agents.update", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mocks.loadConfigReturn = {};
|
||||||
|
mocks.findAgentEntryIndex.mockReturnValue(0);
|
||||||
|
mocks.applyAgentConfig.mockImplementation((_cfg, _opts) => ({}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates an existing agent successfully", async () => {
|
||||||
|
const { respond, promise } = makeCall("agents.update", {
|
||||||
|
agentId: "test-agent",
|
||||||
|
name: "Updated Name",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledWith(true, { ok: true, agentId: "test-agent" }, undefined);
|
||||||
|
expect(mocks.writeConfigFile).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects updating a nonexistent agent", async () => {
|
||||||
|
mocks.findAgentEntryIndex.mockReturnValue(-1);
|
||||||
|
|
||||||
|
const { respond, promise } = makeCall("agents.update", {
|
||||||
|
agentId: "nonexistent",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledWith(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({ message: expect.stringContaining("not found") }),
|
||||||
|
);
|
||||||
|
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ensures workspace when workspace changes", async () => {
|
||||||
|
const { promise } = makeCall("agents.update", {
|
||||||
|
agentId: "test-agent",
|
||||||
|
workspace: "/new/workspace",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(mocks.ensureAgentWorkspace).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not ensure workspace when workspace is unchanged", async () => {
|
||||||
|
const { promise } = makeCall("agents.update", {
|
||||||
|
agentId: "test-agent",
|
||||||
|
name: "Just a rename",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(mocks.ensureAgentWorkspace).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("agents.delete", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mocks.loadConfigReturn = {};
|
||||||
|
mocks.findAgentEntryIndex.mockReturnValue(0);
|
||||||
|
mocks.pruneAgentConfig.mockReturnValue({ config: {}, removedBindings: 2 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes an existing agent and trashes files by default", async () => {
|
||||||
|
const { respond, promise } = makeCall("agents.delete", {
|
||||||
|
agentId: "test-agent",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledWith(
|
||||||
|
true,
|
||||||
|
{ ok: true, agentId: "test-agent", removedBindings: 2 },
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
expect(mocks.writeConfigFile).toHaveBeenCalled();
|
||||||
|
// moveToTrashBestEffort calls fs.access then movePathToTrash for each dir
|
||||||
|
expect(mocks.movePathToTrash).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("skips file deletion when deleteFiles is false", async () => {
|
||||||
|
mocks.fsAccess.mockClear();
|
||||||
|
|
||||||
|
const { respond, promise } = makeCall("agents.delete", {
|
||||||
|
agentId: "test-agent",
|
||||||
|
deleteFiles: false,
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledWith(true, expect.objectContaining({ ok: true }), undefined);
|
||||||
|
// moveToTrashBestEffort should not be called at all
|
||||||
|
expect(mocks.fsAccess).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects deleting the main agent", async () => {
|
||||||
|
const { respond, promise } = makeCall("agents.delete", {
|
||||||
|
agentId: "main",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledWith(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({ message: expect.stringContaining("cannot be deleted") }),
|
||||||
|
);
|
||||||
|
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects deleting a nonexistent agent", async () => {
|
||||||
|
mocks.findAgentEntryIndex.mockReturnValue(-1);
|
||||||
|
|
||||||
|
const { respond, promise } = makeCall("agents.delete", {
|
||||||
|
agentId: "ghost",
|
||||||
|
});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledWith(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({ message: expect.stringContaining("not found") }),
|
||||||
|
);
|
||||||
|
expect(mocks.writeConfigFile).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects invalid params (missing agentId)", async () => {
|
||||||
|
const { respond, promise } = makeCall("agents.delete", {});
|
||||||
|
await promise;
|
||||||
|
|
||||||
|
expect(respond).toHaveBeenCalledWith(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
expect.objectContaining({ message: expect.stringContaining("invalid") }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { GatewayRequestHandlers } from "./types.js";
|
import type { GatewayRequestHandlers } from "./types.js";
|
||||||
import { listAgentIds, resolveAgentWorkspaceDir } from "../../agents/agent-scope.js";
|
import {
|
||||||
|
listAgentIds,
|
||||||
|
resolveAgentDir,
|
||||||
|
resolveAgentWorkspaceDir,
|
||||||
|
} from "../../agents/agent-scope.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_AGENTS_FILENAME,
|
DEFAULT_AGENTS_FILENAME,
|
||||||
DEFAULT_BOOTSTRAP_FILENAME,
|
DEFAULT_BOOTSTRAP_FILENAME,
|
||||||
@@ -12,17 +16,30 @@ import {
|
|||||||
DEFAULT_SOUL_FILENAME,
|
DEFAULT_SOUL_FILENAME,
|
||||||
DEFAULT_TOOLS_FILENAME,
|
DEFAULT_TOOLS_FILENAME,
|
||||||
DEFAULT_USER_FILENAME,
|
DEFAULT_USER_FILENAME,
|
||||||
|
ensureAgentWorkspace,
|
||||||
} from "../../agents/workspace.js";
|
} from "../../agents/workspace.js";
|
||||||
import { loadConfig } from "../../config/config.js";
|
import { movePathToTrash } from "../../browser/trash.js";
|
||||||
import { normalizeAgentId } from "../../routing/session-key.js";
|
import {
|
||||||
|
applyAgentConfig,
|
||||||
|
findAgentEntryIndex,
|
||||||
|
listAgentEntries,
|
||||||
|
pruneAgentConfig,
|
||||||
|
} from "../../commands/agents.config.js";
|
||||||
|
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
||||||
|
import { resolveSessionTranscriptsDirForAgent } from "../../config/sessions/paths.js";
|
||||||
|
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../routing/session-key.js";
|
||||||
|
import { resolveUserPath } from "../../utils.js";
|
||||||
import {
|
import {
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
errorShape,
|
errorShape,
|
||||||
formatValidationErrors,
|
formatValidationErrors,
|
||||||
|
validateAgentsCreateParams,
|
||||||
|
validateAgentsDeleteParams,
|
||||||
validateAgentsFilesGetParams,
|
validateAgentsFilesGetParams,
|
||||||
validateAgentsFilesListParams,
|
validateAgentsFilesListParams,
|
||||||
validateAgentsFilesSetParams,
|
validateAgentsFilesSetParams,
|
||||||
validateAgentsListParams,
|
validateAgentsListParams,
|
||||||
|
validateAgentsUpdateParams,
|
||||||
} from "../protocol/index.js";
|
} from "../protocol/index.js";
|
||||||
import { listAgentsForGateway } from "../session-utils.js";
|
import { listAgentsForGateway } from "../session-utils.js";
|
||||||
|
|
||||||
@@ -123,6 +140,30 @@ function resolveAgentIdOrError(agentIdRaw: string, cfg: ReturnType<typeof loadCo
|
|||||||
return agentId;
|
return agentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeIdentityLine(value: string): string {
|
||||||
|
return value.replace(/\s+/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOptionalStringParam(value: unknown): string | undefined {
|
||||||
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function moveToTrashBestEffort(pathname: string): Promise<void> {
|
||||||
|
if (!pathname) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await fs.access(pathname);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await movePathToTrash(pathname);
|
||||||
|
} catch {
|
||||||
|
// Best-effort: path may already be gone or trash unavailable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const agentsHandlers: GatewayRequestHandlers = {
|
export const agentsHandlers: GatewayRequestHandlers = {
|
||||||
"agents.list": ({ params, respond }) => {
|
"agents.list": ({ params, respond }) => {
|
||||||
if (!validateAgentsListParams(params)) {
|
if (!validateAgentsListParams(params)) {
|
||||||
@@ -141,6 +182,189 @@ export const agentsHandlers: GatewayRequestHandlers = {
|
|||||||
const result = listAgentsForGateway(cfg);
|
const result = listAgentsForGateway(cfg);
|
||||||
respond(true, result, undefined);
|
respond(true, result, undefined);
|
||||||
},
|
},
|
||||||
|
"agents.create": async ({ params, respond }) => {
|
||||||
|
if (!validateAgentsCreateParams(params)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
`invalid agents.create params: ${formatValidationErrors(
|
||||||
|
validateAgentsCreateParams.errors,
|
||||||
|
)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const rawName = String(params.name ?? "").trim();
|
||||||
|
const agentId = normalizeAgentId(rawName);
|
||||||
|
if (agentId === DEFAULT_AGENT_ID) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `"${DEFAULT_AGENT_ID}" is reserved`),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (findAgentEntryIndex(listAgentEntries(cfg), agentId) >= 0) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `agent "${agentId}" already exists`),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceDir = resolveUserPath(String(params.workspace ?? "").trim());
|
||||||
|
|
||||||
|
// Resolve agentDir against the config we're about to persist (vs the pre-write config),
|
||||||
|
// so subsequent resolutions can't disagree about the agent's directory.
|
||||||
|
let nextConfig = applyAgentConfig(cfg, {
|
||||||
|
agentId,
|
||||||
|
name: rawName,
|
||||||
|
workspace: workspaceDir,
|
||||||
|
});
|
||||||
|
const agentDir = resolveAgentDir(nextConfig, agentId);
|
||||||
|
nextConfig = applyAgentConfig(nextConfig, { agentId, agentDir });
|
||||||
|
|
||||||
|
// Ensure workspace & transcripts exist BEFORE writing config so a failure
|
||||||
|
// here does not leave a broken config entry behind.
|
||||||
|
const skipBootstrap = Boolean(nextConfig.agents?.defaults?.skipBootstrap);
|
||||||
|
await ensureAgentWorkspace({ dir: workspaceDir, ensureBootstrapFiles: !skipBootstrap });
|
||||||
|
await fs.mkdir(resolveSessionTranscriptsDirForAgent(agentId), { recursive: true });
|
||||||
|
|
||||||
|
await writeConfigFile(nextConfig);
|
||||||
|
|
||||||
|
// Always write Name to IDENTITY.md; optionally include emoji/avatar.
|
||||||
|
const safeName = sanitizeIdentityLine(rawName);
|
||||||
|
const emoji = resolveOptionalStringParam(params.emoji);
|
||||||
|
const avatar = resolveOptionalStringParam(params.avatar);
|
||||||
|
const identityPath = path.join(workspaceDir, DEFAULT_IDENTITY_FILENAME);
|
||||||
|
const lines = [
|
||||||
|
"",
|
||||||
|
`- Name: ${safeName}`,
|
||||||
|
...(emoji ? [`- Emoji: ${sanitizeIdentityLine(emoji)}`] : []),
|
||||||
|
...(avatar ? [`- Avatar: ${sanitizeIdentityLine(avatar)}`] : []),
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
await fs.appendFile(identityPath, lines.join("\n"), "utf-8");
|
||||||
|
|
||||||
|
respond(true, { ok: true, agentId, name: rawName, workspace: workspaceDir }, undefined);
|
||||||
|
},
|
||||||
|
"agents.update": async ({ params, respond }) => {
|
||||||
|
if (!validateAgentsUpdateParams(params)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
`invalid agents.update params: ${formatValidationErrors(
|
||||||
|
validateAgentsUpdateParams.errors,
|
||||||
|
)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const agentId = normalizeAgentId(String(params.agentId ?? ""));
|
||||||
|
if (findAgentEntryIndex(listAgentEntries(cfg), agentId) < 0) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `agent "${agentId}" not found`),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceDir =
|
||||||
|
typeof params.workspace === "string" && params.workspace.trim()
|
||||||
|
? resolveUserPath(params.workspace.trim())
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const model = resolveOptionalStringParam(params.model);
|
||||||
|
const avatar = resolveOptionalStringParam(params.avatar);
|
||||||
|
|
||||||
|
const nextConfig = applyAgentConfig(cfg, {
|
||||||
|
agentId,
|
||||||
|
...(typeof params.name === "string" && params.name.trim()
|
||||||
|
? { name: params.name.trim() }
|
||||||
|
: {}),
|
||||||
|
...(workspaceDir ? { workspace: workspaceDir } : {}),
|
||||||
|
...(model ? { model } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeConfigFile(nextConfig);
|
||||||
|
|
||||||
|
if (workspaceDir) {
|
||||||
|
const skipBootstrap = Boolean(nextConfig.agents?.defaults?.skipBootstrap);
|
||||||
|
await ensureAgentWorkspace({ dir: workspaceDir, ensureBootstrapFiles: !skipBootstrap });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatar) {
|
||||||
|
const workspace = workspaceDir ?? resolveAgentWorkspaceDir(nextConfig, agentId);
|
||||||
|
await fs.mkdir(workspace, { recursive: true });
|
||||||
|
const identityPath = path.join(workspace, DEFAULT_IDENTITY_FILENAME);
|
||||||
|
await fs.appendFile(identityPath, `\n- Avatar: ${sanitizeIdentityLine(avatar)}\n`, "utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(true, { ok: true, agentId }, undefined);
|
||||||
|
},
|
||||||
|
"agents.delete": async ({ params, respond }) => {
|
||||||
|
if (!validateAgentsDeleteParams(params)) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(
|
||||||
|
ErrorCodes.INVALID_REQUEST,
|
||||||
|
`invalid agents.delete params: ${formatValidationErrors(
|
||||||
|
validateAgentsDeleteParams.errors,
|
||||||
|
)}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cfg = loadConfig();
|
||||||
|
const agentId = normalizeAgentId(String(params.agentId ?? ""));
|
||||||
|
if (agentId === DEFAULT_AGENT_ID) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `"${DEFAULT_AGENT_ID}" cannot be deleted`),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (findAgentEntryIndex(listAgentEntries(cfg), agentId) < 0) {
|
||||||
|
respond(
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
errorShape(ErrorCodes.INVALID_REQUEST, `agent "${agentId}" not found`),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteFiles = typeof params.deleteFiles === "boolean" ? params.deleteFiles : true;
|
||||||
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||||
|
const agentDir = resolveAgentDir(cfg, agentId);
|
||||||
|
const sessionsDir = resolveSessionTranscriptsDirForAgent(agentId);
|
||||||
|
|
||||||
|
const result = pruneAgentConfig(cfg, agentId);
|
||||||
|
await writeConfigFile(result.config);
|
||||||
|
|
||||||
|
if (deleteFiles) {
|
||||||
|
await Promise.all([
|
||||||
|
moveToTrashBestEffort(workspaceDir),
|
||||||
|
moveToTrashBestEffort(agentDir),
|
||||||
|
moveToTrashBestEffort(sessionsDir),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
respond(true, { ok: true, agentId, removedBindings: result.removedBindings }, undefined);
|
||||||
|
},
|
||||||
"agents.files.list": async ({ params, respond }) => {
|
"agents.files.list": async ({ params, respond }) => {
|
||||||
if (!validateAgentsFilesListParams(params)) {
|
if (!validateAgentsFilesListParams(params)) {
|
||||||
respond(
|
respond(
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
|
|
||||||
const fetchImpl = resolveTelegramFetch(opts.proxyFetch, {
|
const fetchImpl = resolveTelegramFetch(opts.proxyFetch, {
|
||||||
network: telegramCfg.network,
|
network: telegramCfg.network,
|
||||||
});
|
}) as unknown as ApiClientOptions["fetch"];
|
||||||
const shouldProvideFetch = Boolean(fetchImpl);
|
const shouldProvideFetch = Boolean(fetchImpl);
|
||||||
// grammY's ApiClientOptions types still track `node-fetch` types; Node 22+ global fetch
|
// grammY's ApiClientOptions types still track `node-fetch` types; Node 22+ global fetch
|
||||||
// (undici) is structurally compatible at runtime but not assignable in TS.
|
// (undici) is structurally compatible at runtime but not assignable in TS.
|
||||||
|
|||||||
Reference in New Issue
Block a user