dsh-plugin-tool-image
v0.3.1
Published
Consumer for image generation in the DeepSeek Harness: the tool the model sees, the route gate, batch admission, and the replayable projection
Maintainers
Readme
dsh-plugin-tool-image
Consumer de geração de imagem para o DeepSeek Harness.
É a tool que o modelo enxerga. Pede imagens pelo vocabulário de dsh-plugin-image, publica o
resultado no armazenamento de anexos do deployment, e devolve ao turno blocos de imagem ou as
referências dos anexos, conforme o deployment escolher. Não
sabe qual provedor está montado, e não pode saber: se algum nome desta superfície passar a
dizer como os bytes viajaram, scripts/check-no-secrets.mjs falha.
Montagem
# cordis.patch.yml do profile
plugins:
dsh-plugin-tool-image:
models:
- google/gemini-2.5-flash-image
returnMode: model-visible| Campo | Obrigatório | Padrão | O que é |
|---|---|---|---|
| models | não | ausente | Enumeração que o modelo enxerga no argumento model. Não vazia e sem repetição quando presente |
| returnMode | não | model-visible | Como o lote produzido volta ao turno. União fechada de dois valores |
Sem models, o argumento model não é declarado e o provedor usa o padrão configurado
nele. Isso é a ausência de um argumento opcional, visível no esquema que o modelo recebe, e
não um fallback escondido.
returnMode
| Valor | Consulta a rota da sessão? | O que volta ao turno |
|---|---|---|
| model-visible | sim, antes de qualquer gasto | um bloco de imagem por imagem produzida |
| reference-only | não | um bloco de texto com os identificadores dos anexos |
O lote é arquivado nos dois modos, inteiro e na ordem do provedor. O que muda é a projeção.
model-visible é o padrão e é o comportamento anterior a este campo: se a rota que serve a
conversa não declara a modalidade image, a chamada é recusada com IMAGE_ROUTE_TEXT_ONLY ou
IMAGE_ROUTE_UNKNOWN antes da chamada paga. A recusa protege uma promessa concreta — a de
devolver conteúdo que o modelo consegue ler.
reference-only existe porque essa promessa nem sempre é a desejada. Um deployment que quer
gerar e arquivar, sem que o modelo enxergue o resultado, não tem por que amarrar a capacidade
ao modelo que serve a conversa. Nesse modo a rota não é consultada, e os dois códigos de recusa
acima deixam de ser alcançáveis — continuam existindo e exportados, para o outro modo.
Um valor fora dos dois faz o carregamento falhar com TypeError nomeando returnMode, antes de
qualquer chamada.
O modo com que uma operação foi atendida é gravado no valor canônico, ao lado de model,
provider e cost. É o que faz uma sessão arquivada ser reexibida como foi gravada, mesmo que
o profile mude de modo depois: a projeção lê o valor, nunca a configuração corrente.
Carregar não toca a rede e não lê ambiente. A única coisa que acontece é a validação da configuração e o registro de dois efeitos — o observador de rota e a própria tool. Os dois disposers seguem o fiber que carregou o plugin.
inject declara tools, attachments e llm. image não está lá, e isso é decisão.
Declará-lo impediria a montagem enquanto nenhum provedor estivesse montado, e quem chamasse a
tool veria capacidade ausente em vez da recusa nomeada que o contrato promete.
O que o modelo vê
| Argumento | Tipo | Obrigatório | Restrição |
|---|---|---|---|
| prompt | string | sim | não vazio depois de trim — checado no corpo |
| model | string com enum | não | membro da enumeração — checado pelo harness |
| aspect_ratio | string | não | livre; o provedor valida contra o catálogo |
| n | integer | não | >= 1 e <= o limite de imagens por mensagem do deployment |
aspect_ratio fica livre de propósito: o domínio dele é por modelo e vem do catálogo, que é
rede. Declará-lo como enumeração exigiria escolher um domínio no carregamento; deixá-lo livre
faz o provedor recusar nomeando o parâmetro, que é o que a fase anterior já entrega.
A ordem, e por que ela é o coração do pacote
1. checagem de corpo -> IMAGE_TOOL_ARGS | IMAGE_BATCH_TOO_LARGE
2. gate de rota -> IMAGE_ROUTE_UNKNOWN | IMAGE_ROUTE_TEXT_ONLY
3. resolução da spec -> ImageRequest completo
4. chamada ao seam -> o único ponto que custa dinheiro
5. admissão -> lote inteiro, ordem preservada
6. projeção -> o valor canônicoToda recusa evitável acontece antes do passo 4, e cada uma nomeia sua causa. Trocar a ordem manteria os testes do caminho feliz verdes e perderia exatamente a propriedade que este pacote existe para entregar.
Não há passo de decodificação. Ele vive no provedor (ADR-0012): o contrato entrega
Uint8Array, e base64 é forma de transporte.
O gate de rota, e o risco que ele carrega
O harness recusa a leitura de imagem quando a rota de modelo ativa não declara image nas
modalidades de entrada. A função que faz isso não é exportada pelo contrato público do pacote
que a contém, então aqui ela é reimplementada (ADR-0007).
A rota ativa é observada no waterfall llm/stream, chaveada pela sessão do agente que chamou.
Requisições com purpose presente são ignoradas: compactação e título de sessão são chamadas
auxiliares, e a rota delas não é a rota do turno.
Três recusas, com causas distintas:
| Situação | Código |
|---|---|
| nenhuma rota observada, ou chamada sem agente | IMAGE_ROUTE_UNKNOWN |
| a rota não declara modalidade alguma | IMAGE_ROUTE_UNKNOWN |
| a rota declara modalidades e image não está entre elas | IMAGE_ROUTE_TEXT_ONLY |
Capacidade desconhecida recusa. Tratá-la como capacidade presente é exatamente a falha que o gate existe para evitar.
Risco conhecido: se o harness mudar a forma de resolveModelInfo, este gate diverge em
silêncio. Duas verificações existem para que isso falhe alto — o pino nominal em
tests/published-surface/surface.ts e o teste estrutural em test/surface-drift.test.ts. A
primeira prova que o símbolo ainda existe assim; a segunda prova que o gate ainda o usa
assim.
Admissão
O lote inteiro vai numa única chamada a AttachmentStore.saveImages, com a ordem preservada.
A semântica de tudo-ou-nada vem de lá, e não é reimplementada aqui: falha de validação não
inicia escrita alguma, e falha de armazenamento não devolve referência parcial. Um laço sobre
saveImage substituiria essa garantia pelo oposto.
admitEncodedImages existe no alvo e não é usada. Ela aceita base64 vindo da wire e então
delega a saveImages; como o contrato entrega bytes, alcançá-la exigiria codificar apenas para
que ela decodificasse de volta (ADR-0012). É escolha de desenho registrada, não ausência de
API.
Um lote maior que o pedido não é compensado aqui: é obrigação declarada do provedor, e compensá-la esconderia o defeito lá. O lote chega inteiro ao armazenamento e é recusado pela própria política quando ultrapassa o limite.
Custo, provedor e modelo
O valor canônico da tool é
{ images: [{ attachmentId, mediaType, bytes, width, height, name? }], model, provider?, cost? }.
O núcleo o persiste junto do resultado, e o replay o relê. Ler custo, provedor e modelo é ler
campo, nunca interpretar prosa (ADR-0008).
Ausência é ausência de campo, nunca 0 nem "".
Consequência aceita: custo de imagem não agrega em nenhum totalizador do harness. Somar exige varrer os resultados de tool — e é justamente por isso que os campos existem.
Apresentação
Card generic, sem locations (ADR-0006). A união publicada oferece generic, terminal,
diff, search, read e web, e nenhum é card de imagem; locations aponta para arquivos
que a tool toca, e esta não toca nenhum — o anexo é endereçado por conteúdo.
As funções de apresentação são puras nos argumentos e totais. Elas rodam ao vivo e de novo no replay do log, então um valor lido de qualquer outro lugar renderizaria de um jeito agora e de outro depois, e um caminho que lance quebraria a renderização de uma sessão antiga inteira.
Falhas
Uma falha que já tinha código a montante mantém o dela: imageFailureCode(kind) para falha do
contrato, AttachmentErrorCode para falha do armazenamento. Inventar código novo a cada camada
tornaria a causa real irrastreável depois de um salto.
Os cinco códigos próprios estão em IMAGE_TOOL_ERROR_CODES, exportado para que um programa
importe a constante em vez de repetir o literal.
Dependências
dependencies é vazio, por decisão. Os pacotes do harness são peerDependencies com intervalo
mais devDependencies pinadas em versão exata (ADR-0011): o intervalo deixa a instalação
prover versão mais nova sem recompilar, e o pino faz o compilador enxergar só o alvo.
Uma nota de implementação
Nenhum estado de classe deste repositório vive em campo privado de ECMAScript (#x). Aqui a
razão não é o proxy de contexto — este plugin não é lido por um — e sim manter a regra uniforme,
o que custa zero e remove a pergunta de qual classe é lida assim.
Uma nota relacionada, e essa é de plataforma: findImageService do contrato usa
ctx.get('image') e não ctx.image. O acessador de propriedade passa pelo proxy de contexto,
que recusa um nome que o fiber chamador não declarou em inject — ele lança
cannot get property "image" without inject em vez de responder undefined. Medido nesta
fase; a saída está em specs/003-consumer-admission-bundle/verification-evidence.md.
