merge: agent-lifecycle-polish
# Conflicts: # TASKS.md
This commit is contained in:
@@ -2,21 +2,19 @@
|
|||||||
|
|
||||||
## Current
|
## Current
|
||||||
|
|
||||||
### `feat/current-workspace-session-model`
|
### `feat/agent-lifecycle-polish`
|
||||||
|
|
||||||
Status: in_progress
|
Status: in_progress
|
||||||
|
|
||||||
DoD:
|
DoD:
|
||||||
- current workspace is stored in authenticated session, not only in localStorage
|
- agents support explicit `publish`, `unpublish`, and `archive` lifecycle transitions
|
||||||
- workspace switcher updates session state through auth API
|
- agents page reflects real backend lifecycle states without legacy `active/inactive` mapping
|
||||||
- shell identity and page data follow session-selected workspace after reload
|
- agent cards and editor can move between draft/published/archived without destructive hacks
|
||||||
- frontend retains only cache/fallback state, not source-of-truth selection
|
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
- `feat/agent-lifecycle-polish`
|
- `feat/platform-key-usage`
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
- `feat/agent-lifecycle-polish`
|
|
||||||
- `feat/platform-key-usage`
|
- `feat/platform-key-usage`
|
||||||
|
|||||||
+118
-2
@@ -12,8 +12,8 @@ use crate::{
|
|||||||
list_memberships, list_platform_api_keys, revoke_platform_api_key, update_membership,
|
list_memberships, list_platform_api_keys, revoke_platform_api_key, update_membership,
|
||||||
},
|
},
|
||||||
agents::{
|
agents::{
|
||||||
create_agent, delete_agent, get_agent, get_agent_version, list_agents, publish_agent,
|
archive_agent, create_agent, delete_agent, get_agent, get_agent_version, list_agents,
|
||||||
save_agent_bindings, update_agent,
|
publish_agent, save_agent_bindings, unpublish_agent, update_agent,
|
||||||
},
|
},
|
||||||
auth::{
|
auth::{
|
||||||
change_password, get_profile, get_session, login, logout, update_current_workspace,
|
change_password, get_profile, get_session, login, logout, update_current_workspace,
|
||||||
@@ -92,6 +92,8 @@ pub fn build_app(state: AppState) -> Router {
|
|||||||
)
|
)
|
||||||
.route("/agents/{agent_id}/bindings", post(save_agent_bindings))
|
.route("/agents/{agent_id}/bindings", post(save_agent_bindings))
|
||||||
.route("/agents/{agent_id}/publish", post(publish_agent))
|
.route("/agents/{agent_id}/publish", post(publish_agent))
|
||||||
|
.route("/agents/{agent_id}/unpublish", post(unpublish_agent))
|
||||||
|
.route("/agents/{agent_id}/archive", post(archive_agent))
|
||||||
.route(
|
.route(
|
||||||
"/auth-profiles",
|
"/auth-profiles",
|
||||||
get(list_auth_profiles).post(create_auth_profile),
|
get(list_auth_profiles).post(create_auth_profile),
|
||||||
@@ -669,6 +671,120 @@ mod tests {
|
|||||||
assert_eq!(missing.status(), reqwest::StatusCode::NOT_FOUND);
|
assert_eq!(missing.status(), reqwest::StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
#[serial]
|
||||||
|
async fn unpublishes_and_archives_agent() {
|
||||||
|
let registry = test_registry().await;
|
||||||
|
let storage_root = test_storage_root("agent_statuses");
|
||||||
|
let upstream_base_url = spawn_upstream_server().await;
|
||||||
|
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||||
|
let client = authorized_client(&base_url).await;
|
||||||
|
|
||||||
|
let operation = assert_success_json(
|
||||||
|
client
|
||||||
|
.post(format!("{base_url}/operations"))
|
||||||
|
.json(&test_operation_payload(
|
||||||
|
&upstream_base_url,
|
||||||
|
"crm_create_lead_agent_status",
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let operation_id = operation["operation_id"].as_str().unwrap().to_owned();
|
||||||
|
|
||||||
|
client
|
||||||
|
.post(format!("{base_url}/operations/{operation_id}/publish"))
|
||||||
|
.json(&json!({ "version": 1 }))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let created = assert_success_json(
|
||||||
|
client
|
||||||
|
.post(format!("{base_url}/agents"))
|
||||||
|
.json(&json!({
|
||||||
|
"slug": "sales-routing",
|
||||||
|
"display_name": "Sales Routing",
|
||||||
|
"description": "Routing agent",
|
||||||
|
"instructions": {},
|
||||||
|
"tool_selection_policy": {}
|
||||||
|
}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let agent_id = created["agent_id"].as_str().unwrap().to_owned();
|
||||||
|
|
||||||
|
client
|
||||||
|
.post(format!("{base_url}/agents/{agent_id}/bindings"))
|
||||||
|
.json(&json!([
|
||||||
|
{
|
||||||
|
"operation_id": operation_id,
|
||||||
|
"operation_version": 1,
|
||||||
|
"tool_name": "crm_create_lead_agent_status",
|
||||||
|
"tool_title": "Create Lead",
|
||||||
|
"tool_description_override": null,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
]))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
client
|
||||||
|
.post(format!("{base_url}/agents/{agent_id}/publish"))
|
||||||
|
.json(&json!({ "version": 1 }))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let unpublished = assert_success_json(
|
||||||
|
client
|
||||||
|
.post(format!("{base_url}/agents/{agent_id}/unpublish"))
|
||||||
|
.json(&json!({}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(unpublished["agent_id"], agent_id);
|
||||||
|
|
||||||
|
let draft_detail = assert_success_json(
|
||||||
|
client
|
||||||
|
.get(format!("{base_url}/agents/{agent_id}"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(draft_detail["status"], "draft");
|
||||||
|
assert_eq!(draft_detail["latest_published_version"], 1);
|
||||||
|
|
||||||
|
let archived = assert_success_json(
|
||||||
|
client
|
||||||
|
.post(format!("{base_url}/agents/{agent_id}/archive"))
|
||||||
|
.json(&json!({}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(archived["agent_id"], agent_id);
|
||||||
|
|
||||||
|
let archived_detail = assert_success_json(
|
||||||
|
client
|
||||||
|
.get(format!("{base_url}/agents/{agent_id}"))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(archived_detail["status"], "archived");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn manages_platform_access_resources() {
|
async fn manages_platform_access_resources() {
|
||||||
|
|||||||
@@ -142,3 +142,31 @@ pub async fn publish_agent(
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(Json(json!(published)))
|
Ok(Json(json!(published)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn unpublish_agent(
|
||||||
|
Path(path): Path<WorkspaceAgentPath>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Json<Value>, ApiError> {
|
||||||
|
let updated = state
|
||||||
|
.service
|
||||||
|
.unpublish_agent(
|
||||||
|
&path.workspace_id.as_str().into(),
|
||||||
|
&path.agent_id.as_str().into(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(json!(updated)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn archive_agent(
|
||||||
|
Path(path): Path<WorkspaceAgentPath>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<Json<Value>, ApiError> {
|
||||||
|
let updated = state
|
||||||
|
.service
|
||||||
|
.archive_agent(
|
||||||
|
&path.workspace_id.as_str().into(),
|
||||||
|
&path.agent_id.as_str().into(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Json(json!(updated)))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2026,6 +2026,46 @@ impl AdminService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str()))]
|
||||||
|
pub async fn unpublish_agent(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
agent_id: &AgentId,
|
||||||
|
) -> Result<AgentMutationResult, ApiError> {
|
||||||
|
self.ensure_workspace_exists(workspace_id).await?;
|
||||||
|
let updated_at = now_string()?;
|
||||||
|
self.registry
|
||||||
|
.unpublish_agent(workspace_id, agent_id, &updated_at)
|
||||||
|
.await?;
|
||||||
|
info!(agent_id = %agent_id.as_str(), "agent moved to draft");
|
||||||
|
|
||||||
|
Ok(AgentMutationResult {
|
||||||
|
agent_id: agent_id.as_str().to_owned(),
|
||||||
|
workspace_id: workspace_id.as_str().to_owned(),
|
||||||
|
updated_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self), fields(workspace_id = %workspace_id.as_str(), agent_id = %agent_id.as_str()))]
|
||||||
|
pub async fn archive_agent(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
agent_id: &AgentId,
|
||||||
|
) -> Result<AgentMutationResult, ApiError> {
|
||||||
|
self.ensure_workspace_exists(workspace_id).await?;
|
||||||
|
let updated_at = now_string()?;
|
||||||
|
self.registry
|
||||||
|
.archive_agent(workspace_id, agent_id, &updated_at)
|
||||||
|
.await?;
|
||||||
|
info!(agent_id = %agent_id.as_str(), "agent archived");
|
||||||
|
|
||||||
|
Ok(AgentMutationResult {
|
||||||
|
agent_id: agent_id.as_str().to_owned(),
|
||||||
|
workspace_id: workspace_id.as_str().to_owned(),
|
||||||
|
updated_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), source_name = source_name.unwrap_or("descriptor-set.bin")))]
|
#[instrument(skip(self, payload), fields(operation_id = %operation_id.as_str(), source_name = source_name.unwrap_or("descriptor-set.bin")))]
|
||||||
pub async fn upload_descriptor_set(
|
pub async fn upload_descriptor_set(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -1051,9 +1051,9 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.agent-status-active { background: rgba(63,185,80,0.15); color: #3fb950; border: 1px solid rgba(63,185,80,0.3); }
|
.agent-status-published { background: rgba(63,185,80,0.15); color: #3fb950; border: 1px solid rgba(63,185,80,0.3); }
|
||||||
.agent-status-draft { background: rgba(139,148,158,0.15); color: #8b949e; border: 1px solid rgba(139,148,158,0.3); }
|
.agent-status-draft { background: rgba(139,148,158,0.15); color: #8b949e; border: 1px solid rgba(139,148,158,0.3); }
|
||||||
.agent-status-inactive{ background: rgba(248,81,73,0.1); color: #f85149; border: 1px solid rgba(248,81,73,0.2); }
|
.agent-status-archived { background: rgba(210,153,34,0.15); color: #d29922; border: 1px solid rgba(210,153,34,0.3); }
|
||||||
|
|
||||||
.agent-card-desc {
|
.agent-card-desc {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
<div class="stat-delta">across this workspace</div>
|
<div class="stat-delta">across this workspace</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">Active</div>
|
<div class="stat-label">Published</div>
|
||||||
<div class="stat-value" x-text="activeCount">—</div>
|
<div class="stat-value" x-text="activeCount">—</div>
|
||||||
<div class="stat-delta" x-text="agents.length ? Math.round(activeCount/agents.length*100)+'% of total' : ''">—</div>
|
<div class="stat-delta" x-text="agents.length ? Math.round(activeCount/agents.length*100)+'% of total' : ''">—</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
<div class="agent-card-name" x-text="agent.display_name"></div>
|
<div class="agent-card-name" x-text="agent.display_name"></div>
|
||||||
<div class="agent-card-slug" x-text="agent.slug"></div>
|
<div class="agent-card-slug" x-text="agent.slug"></div>
|
||||||
</div>
|
</div>
|
||||||
<span :class="statusClass(agent.status)" x-text="agent.status.charAt(0).toUpperCase() + agent.status.slice(1)"></span>
|
<span :class="statusClass(agent.status)" x-text="lifecycleLabel(agent)"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
@@ -243,6 +243,14 @@
|
|||||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 2l3 3-8 8H3v-3l8-8z"/></svg>
|
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11 2l3 3-8 8H3v-3l8-8z"/></svg>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
|
<button class="agent-action-btn" @click.stop="applyLifecycle(agent, lifecycleAction(agent).key)">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 3.5v9l8-4.5-8-4.5z"/></svg>
|
||||||
|
<span x-text="lifecycleAction(agent).label"></span>
|
||||||
|
</button>
|
||||||
|
<button class="agent-action-btn" x-show="agent.raw_status !== 'archived'" @click.stop="applyLifecycle(agent, 'archive')">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 4.5h11v2l-1 6.5h-8l-1-6.5v-2z"/><path d="M5 4.5v-1A1.5 1.5 0 016.5 2h3A1.5 1.5 0 0111 3.5v1"/></svg>
|
||||||
|
Archive
|
||||||
|
</button>
|
||||||
<button class="agent-action-btn danger" @click.stop="deleteAgent(agent.id)">
|
<button class="agent-action-btn danger" @click.stop="deleteAgent(agent.id)">
|
||||||
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
<svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 4h12M5 4V3a1 1 0 011-1h4a1 1 0 011 1v1M10 8v4M6 8v4"/><path d="M3 4l1 9a1 1 0 001 1h6a1 1 0 001-1l1-9"/></svg>
|
||||||
Delete
|
Delete
|
||||||
@@ -306,12 +314,15 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Status</label>
|
<label class="form-label">Status</label>
|
||||||
<div class="agent-status-toggle">
|
<div class="agent-status-toggle">
|
||||||
<button class="agent-status-opt" :class="{ active: form.status === 'active' }" @click="form.status = 'active'">
|
<button class="agent-status-opt" :class="{ active: form.status === 'published' }" @click="form.status = 'published'">
|
||||||
<span class="status-dot" style="background: var(--success, #3fb950)"></span> Active
|
<span class="status-dot" style="background: var(--success, #3fb950)"></span> Published
|
||||||
</button>
|
</button>
|
||||||
<button class="agent-status-opt" :class="{ active: form.status === 'draft' }" @click="form.status = 'draft'">
|
<button class="agent-status-opt" :class="{ active: form.status === 'draft' }" @click="form.status = 'draft'">
|
||||||
<span class="status-dot" style="background: var(--text-muted)"></span> Draft
|
<span class="status-dot" style="background: var(--text-muted)"></span> Draft
|
||||||
</button>
|
</button>
|
||||||
|
<button class="agent-status-opt" :class="{ active: form.status === 'archived' }" @click="form.status = 'archived'">
|
||||||
|
<span class="status-dot" style="background: var(--orange, #d29922)"></span> Archived
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+54
-7
@@ -1,6 +1,6 @@
|
|||||||
function agentUiStatus(status) {
|
function agentUiStatus(status) {
|
||||||
if (status === 'published') return 'active';
|
if (status === 'published') return 'published';
|
||||||
if (status === 'archived') return 'inactive';
|
if (status === 'archived') return 'archived';
|
||||||
return status || 'draft';
|
return status || 'draft';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ document.addEventListener('alpine:init', function() {
|
|||||||
display_name: '',
|
display_name: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
description: '',
|
description: '',
|
||||||
status: 'active',
|
status: 'published',
|
||||||
selectedOps: [],
|
selectedOps: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ document.addEventListener('alpine:init', function() {
|
|||||||
|
|
||||||
get activeCount() {
|
get activeCount() {
|
||||||
return this.agents.filter(function(agent) {
|
return this.agents.filter(function(agent) {
|
||||||
return agent.status === 'active';
|
return agent.status === 'published';
|
||||||
}).length;
|
}).length;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -158,7 +158,7 @@ document.addEventListener('alpine:init', function() {
|
|||||||
display_name: '',
|
display_name: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
description: '',
|
description: '',
|
||||||
status: 'active',
|
status: 'published',
|
||||||
selectedOps: [],
|
selectedOps: [],
|
||||||
};
|
};
|
||||||
this.opSearch = '';
|
this.opSearch = '';
|
||||||
@@ -173,7 +173,7 @@ document.addEventListener('alpine:init', function() {
|
|||||||
display_name: agent.display_name,
|
display_name: agent.display_name,
|
||||||
slug: agent.slug,
|
slug: agent.slug,
|
||||||
description: agent.description,
|
description: agent.description,
|
||||||
status: agent.status === 'active' ? 'active' : 'draft',
|
status: agent.raw_status || agent.status || 'draft',
|
||||||
selectedOps: [].concat(agent.operation_ids || []),
|
selectedOps: [].concat(agent.operation_ids || []),
|
||||||
};
|
};
|
||||||
this.opSearch = '';
|
this.opSearch = '';
|
||||||
@@ -230,6 +230,10 @@ document.addEventListener('alpine:init', function() {
|
|||||||
try {
|
try {
|
||||||
var agentId = this.editingId;
|
var agentId = this.editingId;
|
||||||
var currentVersion = 1;
|
var currentVersion = 1;
|
||||||
|
var existingAgent = this.drawerMode === 'edit'
|
||||||
|
? this.agents.find(function(item) { return item.id === agentId; })
|
||||||
|
: null;
|
||||||
|
var previousStatus = existingAgent ? (existingAgent.raw_status || 'draft') : 'draft';
|
||||||
|
|
||||||
if (this.drawerMode === 'create') {
|
if (this.drawerMode === 'create') {
|
||||||
var created = await window.CrankApi.createAgent(this.workspaceId, {
|
var created = await window.CrankApi.createAgent(this.workspaceId, {
|
||||||
@@ -273,10 +277,14 @@ document.addEventListener('alpine:init', function() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.form.status === 'active') {
|
if (this.form.status === 'published') {
|
||||||
await window.CrankApi.publishAgent(this.workspaceId, agentId, {
|
await window.CrankApi.publishAgent(this.workspaceId, agentId, {
|
||||||
version: currentVersion,
|
version: currentVersion,
|
||||||
});
|
});
|
||||||
|
} else if (this.form.status === 'archived') {
|
||||||
|
await window.CrankApi.archiveAgent(this.workspaceId, agentId);
|
||||||
|
} else if (this.drawerMode === 'edit' && previousStatus !== 'draft') {
|
||||||
|
await window.CrankApi.unpublishAgent(this.workspaceId, agentId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.reload();
|
await this.reload();
|
||||||
@@ -302,6 +310,45 @@ document.addEventListener('alpine:init', function() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async applyLifecycle(agent, action) {
|
||||||
|
if (!this.workspaceId || !window.CrankApi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (action === 'publish') {
|
||||||
|
await window.CrankApi.publishAgent(this.workspaceId, agent.id, {
|
||||||
|
version: agent.current_draft_version || 1,
|
||||||
|
});
|
||||||
|
} else if (action === 'unpublish') {
|
||||||
|
await window.CrankApi.unpublishAgent(this.workspaceId, agent.id);
|
||||||
|
} else if (action === 'archive') {
|
||||||
|
await window.CrankApi.archiveAgent(this.workspaceId, agent.id);
|
||||||
|
}
|
||||||
|
await this.reload();
|
||||||
|
} catch (error) {
|
||||||
|
if (window.CrankUi) {
|
||||||
|
window.CrankUi.error(error.message || 'Failed to update agent lifecycle', 'Lifecycle update failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
lifecycleAction(agent) {
|
||||||
|
if (agent.raw_status === 'published') {
|
||||||
|
return { key: 'unpublish', label: 'Unpublish' };
|
||||||
|
}
|
||||||
|
if (agent.raw_status === 'archived') {
|
||||||
|
return { key: 'unpublish', label: 'Restore draft' };
|
||||||
|
}
|
||||||
|
return { key: 'publish', label: 'Publish' };
|
||||||
|
},
|
||||||
|
|
||||||
|
lifecycleLabel(agent) {
|
||||||
|
if (agent.raw_status === 'published') return 'Published';
|
||||||
|
if (agent.raw_status === 'archived') return 'Archived';
|
||||||
|
return 'Draft';
|
||||||
|
},
|
||||||
|
|
||||||
mcpEndpoint(agent) {
|
mcpEndpoint(agent) {
|
||||||
if (agent.mcp_endpoint) return agent.mcp_endpoint;
|
if (agent.mcp_endpoint) return agent.mcp_endpoint;
|
||||||
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
var workspace = window.getCurrentWorkspace ? window.getCurrentWorkspace() : null;
|
||||||
|
|||||||
@@ -305,6 +305,12 @@
|
|||||||
publishAgent: function(workspaceId, agentId, payload) {
|
publishAgent: function(workspaceId, agentId, payload) {
|
||||||
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/publish', payload);
|
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/publish', payload);
|
||||||
},
|
},
|
||||||
|
unpublishAgent: function(workspaceId, agentId) {
|
||||||
|
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/unpublish', {});
|
||||||
|
},
|
||||||
|
archiveAgent: function(workspaceId, agentId) {
|
||||||
|
return post('/workspaces/' + encodeURIComponent(workspaceId) + '/agents/' + encodeURIComponent(agentId) + '/archive', {});
|
||||||
|
},
|
||||||
listPlatformApiKeys: function(workspaceId) {
|
listPlatformApiKeys: function(workspaceId) {
|
||||||
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/platform-api-keys');
|
return get('/workspaces/' + encodeURIComponent(workspaceId) + '/platform-api-keys');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1549,6 +1549,112 @@ impl PostgresRegistry {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn unpublish_agent(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
agent_id: &AgentId,
|
||||||
|
updated_at: &str,
|
||||||
|
) -> Result<(), RegistryError> {
|
||||||
|
let mut tx = self.pool.begin().await?;
|
||||||
|
|
||||||
|
sqlx::query("delete from published_agents where agent_id = $1")
|
||||||
|
.bind(agent_id.as_str())
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
"update agent_versions
|
||||||
|
set status = $1
|
||||||
|
where agent_id = $2
|
||||||
|
and version = (
|
||||||
|
select current_draft_version
|
||||||
|
from agents
|
||||||
|
where id = $2 and workspace_id = $3
|
||||||
|
)",
|
||||||
|
)
|
||||||
|
.bind(serialize_enum_text(&AgentStatus::Draft, "status")?)
|
||||||
|
.bind(agent_id.as_str())
|
||||||
|
.bind(workspace_id.as_str())
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let result = sqlx::query(
|
||||||
|
"update agents
|
||||||
|
set status = $1,
|
||||||
|
published_at = null,
|
||||||
|
updated_at = $2::timestamptz
|
||||||
|
where id = $3 and workspace_id = $4",
|
||||||
|
)
|
||||||
|
.bind(serialize_enum_text(&AgentStatus::Draft, "status")?)
|
||||||
|
.bind(updated_at)
|
||||||
|
.bind(agent_id.as_str())
|
||||||
|
.bind(workspace_id.as_str())
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
return Err(RegistryError::AgentNotFound {
|
||||||
|
agent_id: agent_id.as_str().to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn archive_agent(
|
||||||
|
&self,
|
||||||
|
workspace_id: &WorkspaceId,
|
||||||
|
agent_id: &AgentId,
|
||||||
|
updated_at: &str,
|
||||||
|
) -> Result<(), RegistryError> {
|
||||||
|
let mut tx = self.pool.begin().await?;
|
||||||
|
|
||||||
|
sqlx::query("delete from published_agents where agent_id = $1")
|
||||||
|
.bind(agent_id.as_str())
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
"update agent_versions
|
||||||
|
set status = $1
|
||||||
|
where agent_id = $2
|
||||||
|
and version = (
|
||||||
|
select current_draft_version
|
||||||
|
from agents
|
||||||
|
where id = $2 and workspace_id = $3
|
||||||
|
)",
|
||||||
|
)
|
||||||
|
.bind(serialize_enum_text(&AgentStatus::Archived, "status")?)
|
||||||
|
.bind(agent_id.as_str())
|
||||||
|
.bind(workspace_id.as_str())
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let result = sqlx::query(
|
||||||
|
"update agents
|
||||||
|
set status = $1,
|
||||||
|
published_at = null,
|
||||||
|
updated_at = $2::timestamptz
|
||||||
|
where id = $3 and workspace_id = $4",
|
||||||
|
)
|
||||||
|
.bind(serialize_enum_text(&AgentStatus::Archived, "status")?)
|
||||||
|
.bind(updated_at)
|
||||||
|
.bind(agent_id.as_str())
|
||||||
|
.bind(workspace_id.as_str())
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
return Err(RegistryError::AgentNotFound {
|
||||||
|
agent_id: agent_id.as_str().to_owned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_published_agent_tools_by_slug(
|
pub async fn get_published_agent_tools_by_slug(
|
||||||
&self,
|
&self,
|
||||||
workspace_slug: &str,
|
workspace_slug: &str,
|
||||||
|
|||||||
@@ -132,6 +132,8 @@
|
|||||||
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/versions`
|
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/versions`
|
||||||
- `GET /api/admin/workspaces/{workspace_id}/agents/{agent_id}/versions/{version}`
|
- `GET /api/admin/workspaces/{workspace_id}/agents/{agent_id}/versions/{version}`
|
||||||
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/publish`
|
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/publish`
|
||||||
|
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/unpublish`
|
||||||
|
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/archive`
|
||||||
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/bindings`
|
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/bindings`
|
||||||
- `DELETE /api/admin/workspaces/{workspace_id}/agents/{agent_id}/bindings/{operation_id}`
|
- `DELETE /api/admin/workspaces/{workspace_id}/agents/{agent_id}/bindings/{operation_id}`
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ UI-файлы:
|
|||||||
- `DELETE /api/admin/workspaces/{workspace_id}/agents/{agent_id}`
|
- `DELETE /api/admin/workspaces/{workspace_id}/agents/{agent_id}`
|
||||||
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/bindings`
|
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/bindings`
|
||||||
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/publish`
|
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/publish`
|
||||||
|
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/unpublish`
|
||||||
|
- `POST /api/admin/workspaces/{workspace_id}/agents/{agent_id}/archive`
|
||||||
- `GET /api/admin/workspaces/{workspace_id}/usage`
|
- `GET /api/admin/workspaces/{workspace_id}/usage`
|
||||||
|
|
||||||
Что еще не хватает:
|
Что еще не хватает:
|
||||||
@@ -172,9 +174,9 @@ UI-файлы:
|
|||||||
|
|
||||||
Простой итог:
|
Простой итог:
|
||||||
|
|
||||||
- базовый live flow уже закрыт: list/create/edit/delete/bind/publish работают через backend;
|
- базовый live flow уже закрыт: list/create/edit/delete/bind/publish/unpublish/archive работают через backend;
|
||||||
- `GET /agents` уже отдает `calls_today`, `key_count`, `operation_count`, `operation_ids` и `mcp_endpoint`;
|
- `GET /agents` уже отдает `calls_today`, `key_count`, `operation_count`, `operation_ids` и `mcp_endpoint`;
|
||||||
- следующий реальный разрыв здесь уже не в CRUD, а в version lifecycle и явном unpublish/archive flow для агентов.
|
- следующий реальный разрыв здесь уже не в CRUD, а в richer version lifecycle поверх этих состояний.
|
||||||
|
|
||||||
### 4.4. API Keys
|
### 4.4. API Keys
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user