Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 139 additions & 29 deletions protos/sift/artifacts/v1/artifacts.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,32 @@ syntax = "proto3";
package sift.artifacts.v1;

import "google/api/annotations.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/timestamp.proto";
import "protoc-gen-openapiv2/options/annotations.proto";
import "sift/metadata/v1/metadata.proto";

option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = {
info: {title: "Artifact Service"}
};

// ArtifactService manages artifacts: independent, org-scoped, versioned
// documents. An artifact is not owned by a conversation. A conversation may
// reference an artifact through a link, any number of conversations may link
// the same artifact, and an artifact may exist with no link at all (for
// example, created over MCP outside an agent session).
// ArtifactService manages an organization-scoped, versioned generic artifact
// store. Content class decides whether content lives in remote_files or the
// version's JSON payload. Attachment and provenance are links, and metadata
// uses the shared metadata values (sift.metadata.v1). No conversation owns an
// artifact.
//
// Links are append-only and belong to the version they were written at. A
// version's rows are its complete link set: ATTACHED_TO rows carry forward to
// each new version, while SOURCE and DERIVED_FROM rows are a closed account of
// one version's inputs. Linking and unlinking write a new version whose
// content is copied from the previous one.
//
// The service carries artifact metadata only. Version bytes live in
// remote_files (entity_type 'artifact_versions', entity_id =
// artifact_version_id): upload via the remote-files multipart endpoint,
// download via RemoteFileService.GetRemoteFileDownloadUrl.
// Artifact bytes live in remote_files (entity_type 'artifact_versions',
// entity_id = artifact_version_id): upload via the remote-files multipart
// endpoint, download via RemoteFileService.GetRemoteFileDownloadUrl.
service ArtifactService {
// Creates an artifact plus its version-1 row, or appends a version when
// Creates an artifact plus its version-1 entry, or appends a version when
// artifact_id is set. When conversation_id is set on create, the new
// artifact is also linked to that conversation.
rpc CreateArtifact(CreateArtifactRequest) returns (CreateArtifactResponse) {
Expand All @@ -48,16 +55,19 @@ service ArtifactService {

// One entry per artifact, resolved to its latest version, oldest first.
// With conversation_id set, only artifacts linked to that conversation;
// otherwise every artifact created by the caller.
// otherwise every artifact in the caller's organization.
rpc ListArtifacts(ListArtifactsRequest) returns (ListArtifactsResponse) {
option (google.api.http) = {get: "/api/v1/artifacts"};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "ListArtifacts"
description: "List artifacts created by the caller, optionally filtered to one conversation."
description: "List artifacts in the caller's organization, optionally filtered to one conversation."
operation_id: "ArtifactService_ListArtifactsV1"
};
}

// Returns the fields available to ListArtifacts filter, including the
// organization's metadata keys as one metadata namespace.

// Full version history of one artifact, newest first.
rpc ListArtifactVersions(ListArtifactVersionsRequest) returns (ListArtifactVersionsResponse) {
option (google.api.http) = {get: "/api/v1/artifacts/{artifact_id}/versions"};
Expand All @@ -68,40 +78,51 @@ service ArtifactService {
};
}

// Links an existing artifact to a conversation. Idempotent: linking an
// already-linked pair succeeds without effect.
// Links an artifact to an entity by writing a new version that carries the
// previous ATTACHED_TO rows plus this link. An identical link already in the
// latest version succeeds without writing a version.

// Revokes an ATTACHED_TO link by writing a new version that omits it. The
// previous version's rows stay in place. A link missing from the latest
// version succeeds without writing a version. SOURCE and DERIVED_FROM rows
// are a closed account of one version's inputs and cannot be unlinked
// (InvalidArgument).

// Lists the links of one artifact version, the latest by default.

// Links an existing artifact to a conversation. Equivalent to LinkArtifact
// with relation ATTACHED_TO and entity_type "conversations". Idempotent:
// linking an already-linked pair succeeds without effect.
rpc LinkArtifactToConversation(LinkArtifactToConversationRequest) returns (LinkArtifactToConversationResponse) {
option (google.api.http) = {
post: "/api/v1/artifacts/{artifact_id}/conversations/{conversation_id}:link"
};
option (google.api.http) = {post: "/api/v1/artifacts/{artifact_id}/conversations/{conversation_id}:link"};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "LinkArtifactToConversation"
description: "Link an existing artifact to a conversation."
operation_id: "ArtifactService_LinkArtifactToConversationV1"
};
}

// Removes one conversation's link to an artifact. The artifact itself is
// untouched. Idempotent: unlinking a missing link succeeds.
// Removes one conversation's link to an artifact. Equivalent to
// UnlinkArtifact with relation ATTACHED_TO and entity_type "conversations".
// The artifact itself is untouched. Idempotent: unlinking a missing link
// succeeds.
rpc UnlinkArtifactFromConversation(UnlinkArtifactFromConversationRequest) returns (UnlinkArtifactFromConversationResponse) {
option (google.api.http) = {
post: "/api/v1/artifacts/{artifact_id}/conversations/{conversation_id}:unlink"
};
option (google.api.http) = {post: "/api/v1/artifacts/{artifact_id}/conversations/{conversation_id}:unlink"};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "UnlinkArtifactFromConversation"
description: "Remove a conversation's link to an artifact."
operation_id: "ArtifactService_UnlinkArtifactFromConversationV1"
};
}

// Sets archived_date. Versions, stored bytes, and conversation links are
// Sets archived_date. Versions, bytes, and links are
// left in place. Idempotent: archiving an already-archived artifact
// succeeds without changing archived_date.
rpc ArchiveArtifact(ArchiveArtifactRequest) returns (ArchiveArtifactResponse) {
option (google.api.http) = {post: "/api/v1/artifacts/{artifact_id}/archive"};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
summary: "ArchiveArtifact"
description: "Archive an artifact. Versions, stored bytes, and conversation links are left in place."
description: "Archive an artifact. Versions, bytes, and links are left in place."
operation_id: "ArtifactService_ArchiveArtifactV1"
};
}
Expand All @@ -124,6 +145,40 @@ enum ArtifactAuthoringKind {
ARTIFACT_AUTHORING_KIND_USER = 2;
}

enum ArtifactStorageClass {
// The content class is not specified.
ARTIFACT_STORAGE_CLASS_UNSPECIFIED = 0;
// Bytes in remote_files, previewable by MIME type.
ARTIFACT_STORAGE_CLASS_FILE = 1;
// Content is the JSON payload, no file.
ARTIFACT_STORAGE_CLASS_STRUCTURED = 2;
// Bytes in remote_files, opaque, download only.
ARTIFACT_STORAGE_CLASS_BLOB = 3;
}

// Which surface wrote the artifact, independent of authoring_kind (who is accountable).
enum ArtifactCreatedVia {
// The producing surface is not specified.
ARTIFACT_CREATED_VIA_UNSPECIFIED = 0;
// An agent wrote the artifact.
ARTIFACT_CREATED_VIA_AGENT = 1;
// Canvas wrote the artifact.
ARTIFACT_CREATED_VIA_CANVAS = 2;
// A direct upload wrote the artifact.
ARTIFACT_CREATED_VIA_UPLOAD = 3;
}

enum ArtifactLinkRelation {
// The link relation is not specified.
ARTIFACT_LINK_RELATION_UNSPECIFIED = 0;
// Placement, where the artifact surfaces.
ARTIFACT_LINK_RELATION_ATTACHED_TO = 1;
// Provenance, what the artifact was produced from; immutable.
ARTIFACT_LINK_RELATION_SOURCE = 2;
// Artifact-to-artifact.
ARTIFACT_LINK_RELATION_DERIVED_FROM = 3;
}

// Container fields flattened together with one resolved version.
message Artifact {
string artifact_id = 1;
Expand All @@ -143,12 +198,18 @@ message Artifact {
// Unset until bytes are uploaded.
optional string remote_file_id = 13;
google.protobuf.Timestamp version_created_date = 14;
// From the version's remote_files row. Unset until bytes are uploaded.
// From the version's remote_files record. Unset until bytes are uploaded.
// Clients infer preview behavior from file_name / file_mime_type.
optional string file_name = 15;
optional string file_mime_type = 16;
// Unset while the artifact is active.
optional google.protobuf.Timestamp archived_date = 17;
ArtifactStorageClass storage_class = 18;
ArtifactCreatedVia created_via = 19;
// Set by GetArtifact for STRUCTURED artifacts. ListArtifacts omits it.
optional google.protobuf.Struct payload = 20;
// Metadata attached to the resolved version.
repeated sift.metadata.v1.MetadataValue metadata = 21;
}

message ArtifactVersion {
Expand All @@ -161,24 +222,54 @@ message ArtifactVersion {
repeated string source_tool_use_ids = 7;
optional string remote_file_id = 8;
google.protobuf.Timestamp created_date = 9;
// From the version's remote_files row. Unset until bytes are uploaded.
// From the version's remote_files record. Unset until bytes are uploaded.
optional string file_name = 10;
optional string file_mime_type = 11;
// Set by GetArtifact for STRUCTURED artifacts. ListArtifactVersions omits it.
optional google.protobuf.Struct payload = 12;
// Metadata attached to this version.
repeated sift.metadata.v1.MetadataValue metadata = 13;
}

// A link from an artifact to another entity, belonging to one version.

message ArtifactLinkInput {
ArtifactLinkRelation relation = 1;
// An open string accepting lowercase values: conversations, canvases, runs,
// assets, artifacts, or tool_uses. entity_id is opaque: a UUID for Sift
// entities, or the provider tool_use_id for tool_uses.
string entity_type = 2;
string entity_id = 3;
}

message CreateArtifactRequest {
// Set to append a version to an existing artifact; unset to create one.
optional string artifact_id = 1;
// Set to link the new artifact to a conversation at create time. Legal
// only on create (not on append), and only for the conversation's author.
// Equivalent to a links entry with relation ATTACHED_TO, entity_type
// "conversations", and this conversation_id as entity_id.
optional string conversation_id = 2;
optional string title = 4;
optional string summary = 5;
// Defaults to USER. AGENT is self-reported: the agent pod authenticates
// with the requesting user's transient key, so until scoped pod
// credentials land (ENG-12831) authorship attribution rides on the same
// credentials land, authorship attribution rides on the same
// trust as the pod's message persistence path.
optional ArtifactAuthoringKind authoring_kind = 6;
// A container field. Legal on create and defaults to FILE when unset. On
// append, it must match the container.
optional ArtifactStorageClass storage_class = 7;
// A container field. Required on create. Ignored on append.
optional ArtifactCreatedVia created_via = 8;
// Required for STRUCTURED and rejected otherwise. The serialized payload
// must not exceed 1 MiB.
optional google.protobuf.Struct payload = 9;
// Links written at the new version. On append, the previous version's
// ATTACHED_TO rows carry forward and these are added to them.
repeated ArtifactLinkInput links = 10;
// Metadata attached to the created version.
repeated sift.metadata.v1.MetadataValue metadata = 11;
}

message CreateArtifactResponse {
Expand All @@ -195,12 +286,31 @@ message GetArtifactResponse {
}

message ListArtifactsRequest {
// Unset lists every artifact created by the caller.
// Unset lists every artifact in the caller's organization.
optional string conversation_id = 1;
uint32 page_size = 2;
string page_token = 3;
// When false (the default), archived artifacts are omitted.
bool include_archived = 4;
// A [Common Expression Language (CEL)](https://github.com/google/cel-spec)
// filter string. Available fields are artifact_id, organization_id,
// created_by_user_id, authoring_kind, storage_class, created_via,
// title, version, created_date, archived_date, the include_archived
// directive, and metadata through `metadata["<key>"]`. Links can be filtered
// with a comprehension such as `links.exists(l, l.relation == "ATTACHED_TO"
// && l.entity_type == "conversations" && l.entity_id == "<id>")`. Link
// sub-fields are relation, entity_type, entity_id, and artifact_version_id.
// Enum fields compare against proto value names without the prefix, for
// example `storage_class == "STRUCTURED"`. Link relation compares against
// the uppercase proto names ATTACHED_TO, SOURCE, and DERIVED_FROM. The
// conversation_id and include_archived fields on this message keep working
// and combine with filter.
string filter = 5;
// A comma-separated [AIP-132](https://google.aip.dev/132#ordering) ordering
// string over created_date, archived_date, title, and version. Fields
// sort ascending by default and support a `desc` suffix. The default is
// created_date ascending.
string order_by = 6;
}

message ListArtifactsResponse {
Expand Down
3 changes: 3 additions & 0 deletions rust/crates/sift_cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### What's New

- Artifact MCP tools now support generic artifact storage classes, structured
JSON payloads, metadata, links, CEL filtering, and ordering.

## [v0.5.0] - September 3, 2026

### What's New
Expand Down
22 changes: 12 additions & 10 deletions rust/crates/sift_cli/assets/skills/sift/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,18 @@ exists.
Send a rename on its own. The API applies a `name` change by itself and
ignores every other field, so `update_user_defined_function` rejects `name`
combined with anything else.
- **Create an artifact.** `create_artifact` with a `title` / `summary`, and
`file_path` pointing at the local file that is the artifact's content — an
artifact without a file has nothing to preview or download. Pass
`conversation_id` to link it to a chat, and `authoring_kind=agent` when
a Sift agent produced it. Append a version by passing the existing
`artifact_id`. One artifact per real deliverable; never one per scratch
file. Creating is gated by `--allow-create`; appending a version to
an existing artifact also needs `--allow-destructive`. Discover artifacts
with `list_artifacts` (oldest first, no `order_by`); fetch a version or its
`download_url` with `download_artifact`.
- **Create an artifact.** `create_artifact` accepts `title`, `summary`,
`kind`, metadata, links, and either `file_path` or a structured JSON
`payload`. Choose `storage_class=file` for previewable files,
`storage_class=structured` for computed tables and PSD-like results, or
`storage_class=blob` for opaque intermediates. `structured` requires a
payload and rejects `file_path`. Set `created_via=chat` inside a Sift agent
session, otherwise use `sdk`. Pass `conversation_id` to link a new artifact
to a chat, and `authoring_kind=agent` when a Sift agent produced it. Append
a version with `artifact_id`. Creating needs `--allow-create`; appending
needs `--allow-destructive`. Use `list_artifacts` with CEL `filter` and
`order_by` such as `created_date desc`; fetch a version or its `download_url`
with `download_artifact`.
- **Produce a chart.** Build a link with `explore_url`. When the user wants a
chart and numbers, do both and give the user both.
- **Answer a question about how Sift works.** Call `search_docs`. Do not answer
Expand Down
Loading