@mawaru/sdk 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_schemas/ai-manifest.test.ts +8 -0
- package/_schemas/ai-manifest.ts +2 -0
- package/_schemas/graph.test.ts +36 -0
- package/_schemas/graph.ts +3 -2
- package/_schemas/node.ts +4 -0
- package/_schemas/port-spec.test.ts +124 -0
- package/_schemas/port-spec.ts +67 -2
- package/dist/_schemas/ai-manifest.d.ts +3 -0
- package/dist/_schemas/ai-manifest.js +2 -0
- package/dist/_schemas/graph.d.ts +2 -0
- package/dist/_schemas/graph.js +3 -2
- package/dist/_schemas/node.d.ts +1 -0
- package/dist/_schemas/node.js +4 -0
- package/dist/_schemas/port-spec.d.ts +2 -0
- package/dist/_schemas/port-spec.js +57 -2
- package/docs/development.md +5 -3
- package/package.json +1 -1
- package/skills/create-loop/SKILL.md +54 -16
|
@@ -145,6 +145,14 @@ describe("aiManifestSchema", () => {
|
|
|
145
145
|
expect(parsed.inputs.instruction?.resume).toBe(true)
|
|
146
146
|
})
|
|
147
147
|
|
|
148
|
+
it("inputs の flexible(推奨型を維持して任意の型を受ける)が読める", () => {
|
|
149
|
+
const parsed = aiManifestSchema.parse({
|
|
150
|
+
...valid,
|
|
151
|
+
inputs: { in: { schema: { type: "object" }, flexible: true } },
|
|
152
|
+
})
|
|
153
|
+
expect(parsed.inputs.in?.flexible).toBe(true)
|
|
154
|
+
})
|
|
155
|
+
|
|
148
156
|
it("旧形式(値が素の JSON Schema)は { schema: 値 } として読める", () => {
|
|
149
157
|
const legacy = aiManifestSchema.parse({
|
|
150
158
|
...valid,
|
package/_schemas/ai-manifest.ts
CHANGED
|
@@ -72,6 +72,8 @@ export const agentOfModel = (model: AiModel): AiAgentId =>
|
|
|
72
72
|
export const aiInputDeclSchema = z.object({
|
|
73
73
|
schema: jsonSchemaSchema,
|
|
74
74
|
resume: z.boolean().optional(),
|
|
75
|
+
// 推奨型を維持して任意の型を受ける(接続で上書きされず、適合チェックもしない)
|
|
76
|
+
flexible: z.boolean().optional(),
|
|
75
77
|
})
|
|
76
78
|
export type AiInputDecl = z.infer<typeof aiInputDeclSchema>
|
|
77
79
|
|
package/_schemas/graph.test.ts
CHANGED
|
@@ -1203,6 +1203,42 @@ describe("saveGraphBodySchema:コンポーネントノード(component kind
|
|
|
1203
1203
|
).not.toThrow()
|
|
1204
1204
|
})
|
|
1205
1205
|
|
|
1206
|
+
it("flexible な in(契約で焼き込まれた component.in・AI の宣言 in)への接続は型が違っても保存できる", () => {
|
|
1207
|
+
const prog = {
|
|
1208
|
+
...makeNode([
|
|
1209
|
+
port("in", "in"),
|
|
1210
|
+
{ ...port("out", "main"), schema: { type: "string" } },
|
|
1211
|
+
]),
|
|
1212
|
+
kind: "program" as const,
|
|
1213
|
+
}
|
|
1214
|
+
const comp = {
|
|
1215
|
+
...componentNode({ type: "number" }),
|
|
1216
|
+
}
|
|
1217
|
+
comp.ports = comp.ports.map((p) =>
|
|
1218
|
+
p.io === "in" ? { ...p, flexible: true } : p,
|
|
1219
|
+
)
|
|
1220
|
+
expect(() =>
|
|
1221
|
+
saveGraphBodySchema.parse({
|
|
1222
|
+
nodes: [prog, comp],
|
|
1223
|
+
connections: [connect(prog, comp)],
|
|
1224
|
+
}),
|
|
1225
|
+
).not.toThrow()
|
|
1226
|
+
// AI の flexible な in:鏡映されず推奨型 {type:"number"} が残るが、型が違う接続を通す
|
|
1227
|
+
const ai = {
|
|
1228
|
+
...makeNode([
|
|
1229
|
+
{ ...port("in", "in"), schema: { type: "number" }, flexible: true },
|
|
1230
|
+
port("out", "main"),
|
|
1231
|
+
]),
|
|
1232
|
+
kind: "ai" as const,
|
|
1233
|
+
}
|
|
1234
|
+
expect(() =>
|
|
1235
|
+
saveGraphBodySchema.parse({
|
|
1236
|
+
nodes: [prog, ai],
|
|
1237
|
+
connections: [connect(prog, ai)],
|
|
1238
|
+
}),
|
|
1239
|
+
).not.toThrow()
|
|
1240
|
+
})
|
|
1241
|
+
|
|
1206
1242
|
it("component の in への接続は契約への適合を検証する(伝播型 ⊆ 契約)", () => {
|
|
1207
1243
|
// 上流 program の out {type:"string"} → component の in {type:"number"} は不適合
|
|
1208
1244
|
const prog = {
|
package/_schemas/graph.ts
CHANGED
|
@@ -6,8 +6,8 @@ import {
|
|
|
6
6
|
portKeySchema,
|
|
7
7
|
} from "./node.js"
|
|
8
8
|
import {
|
|
9
|
+
connectionAccepted,
|
|
9
10
|
hasInboundConflict,
|
|
10
|
-
isSchemaSubset,
|
|
11
11
|
kindPortsViolation,
|
|
12
12
|
resolvePortSchemas,
|
|
13
13
|
UNDECLARED_SCHEMA,
|
|
@@ -551,9 +551,10 @@ export const saveGraphBodySchema = z
|
|
|
551
551
|
// 接続鏡映する口(program / ai の in)とコピー宣言のビューは接続元の型を採用する
|
|
552
552
|
// ので自明に通り、実質の検証対象は宣言で固定された口
|
|
553
553
|
// (component の契約・宣言のあるビューの human.in)になる
|
|
554
|
+
// flexible な in(AI の宣言 in・契約で焼き込まれた component の in)は型を問わない
|
|
554
555
|
const fromSchema = resolved.get(conn.from_port_id) ?? from.schema
|
|
555
556
|
const toSchema = resolved.get(conn.to_port_id) ?? to.schema
|
|
556
|
-
if (!
|
|
557
|
+
if (!connectionAccepted(fromSchema, { ...to, schema: toSchema })) {
|
|
557
558
|
ctx.addIssue({
|
|
558
559
|
code: "custom",
|
|
559
560
|
path: ["connections", i, "to_port_id"],
|
package/_schemas/node.ts
CHANGED
|
@@ -54,6 +54,10 @@ export const graphPortSchema = z
|
|
|
54
54
|
// 再開する(AI ノードで意味を持つ。key ではなくこの属性が再開を決める。
|
|
55
55
|
// ポート型固定の撤去とセッション再開のポート属性化.md)
|
|
56
56
|
resume: z.boolean().optional(),
|
|
57
|
+
// in ポートの型属性:true なら宣言(推奨型)を維持し、接続元の型による上書き・
|
|
58
|
+
// 適合チェックをしない(AI の in で設定。start / component へは導出で流れる。
|
|
59
|
+
// AIノード入力のflexible属性(任意の型で呼べるループ).md)
|
|
60
|
+
flexible: z.boolean().optional(),
|
|
57
61
|
side: portSideSchema.optional(),
|
|
58
62
|
offset: z.number().min(0).max(1).optional(),
|
|
59
63
|
})
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"
|
|
|
2
2
|
import type { GraphPort, JsonSchema } from "./node.js"
|
|
3
3
|
import {
|
|
4
4
|
arrayOf,
|
|
5
|
+
connectionAccepted,
|
|
5
6
|
dataFormatOf,
|
|
6
7
|
dataSchemaOf,
|
|
7
8
|
fileSchema,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
kindPortsViolation,
|
|
13
14
|
mergeInboundSchemas,
|
|
14
15
|
propagateDerivedPorts,
|
|
16
|
+
resolveFlexiblePorts,
|
|
15
17
|
resolvePortSchemas,
|
|
16
18
|
schemaFit,
|
|
17
19
|
schemaTypeKey,
|
|
@@ -661,6 +663,128 @@ describe("型伝播(導出ポートの解決)", () => {
|
|
|
661
663
|
expect(resolved.get(start.outMain.id)).toEqual(UNDECLARED_SCHEMA)
|
|
662
664
|
})
|
|
663
665
|
|
|
666
|
+
// flexible:AI の in の型属性。宣言(推奨型)を維持し、接続の型を問わない。
|
|
667
|
+
// start へは型と同じ経路で導出される(AIノード入力のflexible属性(任意の型で呼べるループ).md)
|
|
668
|
+
describe("flexible(推奨型を維持して任意の型を受ける in)", () => {
|
|
669
|
+
const flexibleAi = (declared: JsonSchema) => {
|
|
670
|
+
const inPort: GraphPort = {
|
|
671
|
+
...port("in", "in", declared),
|
|
672
|
+
flexible: true,
|
|
673
|
+
}
|
|
674
|
+
return {
|
|
675
|
+
node: { id: uuid(), kind: "ai", ports: [inPort, port("out", "main")] },
|
|
676
|
+
inPort,
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
const recommended = {
|
|
680
|
+
type: "object",
|
|
681
|
+
properties: { prompt: { type: "string" } },
|
|
682
|
+
required: ["prompt"],
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
it("flexible な in は接続元の具体型で上書きしない(宣言=推奨型を維持)", () => {
|
|
686
|
+
const program = programNode({ type: "string" })
|
|
687
|
+
const progOut = program.node.ports.find(
|
|
688
|
+
(p) => p.io === "out",
|
|
689
|
+
) as GraphPort
|
|
690
|
+
progOut.schema = declaredOut
|
|
691
|
+
const ai = flexibleAi(recommended)
|
|
692
|
+
const resolved = resolvePortSchemas(
|
|
693
|
+
[program.node, ai.node],
|
|
694
|
+
[{ from_port_id: progOut.id, to_port_id: ai.inPort.id }],
|
|
695
|
+
)
|
|
696
|
+
expect(resolved.get(ai.inPort.id)).toEqual(recommended)
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it("flexible な in は型の割れた fan-in でも推奨型を維持する", () => {
|
|
700
|
+
const a = programNode({ type: "string" })
|
|
701
|
+
const aOut = a.node.ports.find((p) => p.io === "out") as GraphPort
|
|
702
|
+
aOut.schema = { type: "string" }
|
|
703
|
+
const b = programNode({ type: "string" })
|
|
704
|
+
const bOut = b.node.ports.find((p) => p.io === "out") as GraphPort
|
|
705
|
+
bOut.schema = { type: "number" }
|
|
706
|
+
const ai = flexibleAi(recommended)
|
|
707
|
+
const resolved = resolvePortSchemas(
|
|
708
|
+
[a.node, b.node, ai.node],
|
|
709
|
+
[
|
|
710
|
+
{ from_port_id: aOut.id, to_port_id: ai.inPort.id },
|
|
711
|
+
{ from_port_id: bOut.id, to_port_id: ai.inPort.id },
|
|
712
|
+
],
|
|
713
|
+
)
|
|
714
|
+
expect(resolved.get(ai.inPort.id)).toEqual(recommended)
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
it("start → AI(flexible) 直結:start の in / out main が推奨型で flexible になる", () => {
|
|
718
|
+
const start = startNode()
|
|
719
|
+
const ai = flexibleAi(recommended)
|
|
720
|
+
const connections = [
|
|
721
|
+
{ from_port_id: start.outMain.id, to_port_id: ai.inPort.id },
|
|
722
|
+
]
|
|
723
|
+
expect(resolveFlexiblePorts([start.node, ai.node], connections)).toEqual(
|
|
724
|
+
new Set([start.inPort.id, start.outMain.id]),
|
|
725
|
+
)
|
|
726
|
+
const propagated = propagateDerivedPorts(
|
|
727
|
+
[start.node, ai.node],
|
|
728
|
+
connections,
|
|
729
|
+
)[0]
|
|
730
|
+
if (!propagated) throw new Error("start not propagated")
|
|
731
|
+
for (const p of propagated.ports) {
|
|
732
|
+
expect(p.schema).toEqual(recommended)
|
|
733
|
+
expect(p.flexible).toBe(true)
|
|
734
|
+
}
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
it("start → [AI(flexible), program]:非 flexible の接続先が混ざれば start は flexible にならない", () => {
|
|
738
|
+
const start = startNode()
|
|
739
|
+
const ai = flexibleAi(recommended)
|
|
740
|
+
const program = programNode(recommended)
|
|
741
|
+
const connections = [
|
|
742
|
+
{ from_port_id: start.outMain.id, to_port_id: ai.inPort.id },
|
|
743
|
+
{ from_port_id: start.outMain.id, to_port_id: program.inPort.id },
|
|
744
|
+
]
|
|
745
|
+
expect(
|
|
746
|
+
resolveFlexiblePorts([start.node, ai.node, program.node], connections),
|
|
747
|
+
).toEqual(new Set())
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
it("start:未接続なら flexible にならず、ports に残った古い flexible は落とす", () => {
|
|
751
|
+
const inPort: GraphPort = { ...port("in", "in"), flexible: true }
|
|
752
|
+
const outMain: GraphPort = { ...port("out", "main"), flexible: true }
|
|
753
|
+
const start = { id: uuid(), kind: "start", ports: [inPort, outMain] }
|
|
754
|
+
const propagated = propagateDerivedPorts([start], [])[0]
|
|
755
|
+
if (!propagated) throw new Error("start not propagated")
|
|
756
|
+
for (const p of propagated.ports) expect(p.flexible).toBeUndefined()
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
it("component の in(契約の焼き込みで flexible)に直結した start も flexible(推移)", () => {
|
|
760
|
+
const start = startNode()
|
|
761
|
+
const compIn: GraphPort = {
|
|
762
|
+
...port("in", "in", recommended),
|
|
763
|
+
flexible: true,
|
|
764
|
+
}
|
|
765
|
+
const component = {
|
|
766
|
+
id: uuid(),
|
|
767
|
+
kind: "component",
|
|
768
|
+
ports: [compIn, port("out", "main")],
|
|
769
|
+
}
|
|
770
|
+
expect(
|
|
771
|
+
resolveFlexiblePorts(
|
|
772
|
+
[start.node, component],
|
|
773
|
+
[{ from_port_id: start.outMain.id, to_port_id: compIn.id }],
|
|
774
|
+
),
|
|
775
|
+
).toEqual(new Set([start.inPort.id, start.outMain.id]))
|
|
776
|
+
})
|
|
777
|
+
|
|
778
|
+
it("connectionAccepted:受け側が flexible なら型を問わず可、そうでなければ isSchemaSubset", () => {
|
|
779
|
+
const to = port("in", "in", recommended)
|
|
780
|
+
expect(connectionAccepted({ type: "string" }, to)).toBe(false)
|
|
781
|
+
expect(
|
|
782
|
+
connectionAccepted({ type: "string" }, { ...to, flexible: true }),
|
|
783
|
+
).toBe(true)
|
|
784
|
+
expect(connectionAccepted(recommended, to)).toBe(true)
|
|
785
|
+
})
|
|
786
|
+
})
|
|
787
|
+
|
|
664
788
|
it("propagateDerivedPorts:schema を書き戻し、変化のないノードは同じ参照を返す", () => {
|
|
665
789
|
const progOut = port("out", "main", declaredOut)
|
|
666
790
|
const program = {
|
package/_schemas/port-spec.ts
CHANGED
|
@@ -487,6 +487,8 @@ export const resolvePortSchemas = <N extends PropagationNode>(
|
|
|
487
487
|
if (isMirror) {
|
|
488
488
|
for (const port of node.ports) {
|
|
489
489
|
if (port.io !== "in") continue
|
|
490
|
+
// flexible な in は宣言(推奨型)を維持する(接続の型を問わない口)
|
|
491
|
+
if (port.flexible === true) continue
|
|
490
492
|
const sources = inbound.get(port.id) ?? []
|
|
491
493
|
const inSchema = mergeInboundSchemas(
|
|
492
494
|
sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA),
|
|
@@ -535,19 +537,82 @@ export const resolvePortSchemas = <N extends PropagationNode>(
|
|
|
535
537
|
|
|
536
538
|
// ノード配列へ伝播結果を書き戻す(接続変更時のポートデータ自動更新)。
|
|
537
539
|
// schema が変わらないポート・ノードは同じ参照を返す
|
|
540
|
+
// flexible の導出:start の in / out main は、接続先の in が1つ以上あり**全て** flexible
|
|
541
|
+
// (AI の宣言 in・契約で焼き込まれた component の in)のときだけ flexible。
|
|
542
|
+
// 非 flexible の接続先(program 等)が1つでも混ざれば型どおりの入力が要るので false。
|
|
543
|
+
// 直結のみ(wait 等の素通し経由は型と同じく流さない)。戻りは flexible になる port_id の集合
|
|
544
|
+
export const resolveFlexiblePorts = (
|
|
545
|
+
nodes: PropagationNode[],
|
|
546
|
+
connections: PortLink[],
|
|
547
|
+
): Set<string> => {
|
|
548
|
+
const flexibleByPortId = new Map<string, boolean>()
|
|
549
|
+
for (const node of nodes) {
|
|
550
|
+
if (node.kind === "start") continue
|
|
551
|
+
for (const port of node.ports) {
|
|
552
|
+
if (port.io === "in")
|
|
553
|
+
flexibleByPortId.set(port.id, port.flexible === true)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const outbound = new Map<string, string[]>()
|
|
557
|
+
for (const conn of connections) {
|
|
558
|
+
const outs = outbound.get(conn.from_port_id) ?? []
|
|
559
|
+
outs.push(conn.to_port_id)
|
|
560
|
+
outbound.set(conn.from_port_id, outs)
|
|
561
|
+
}
|
|
562
|
+
const flexible = new Set<string>()
|
|
563
|
+
for (const node of nodes) {
|
|
564
|
+
if (node.kind !== "start") continue
|
|
565
|
+
const inPort = node.ports.find((p) => p.io === "in")
|
|
566
|
+
const outMain = node.ports.find((p) => p.io === "out" && p.key === "main")
|
|
567
|
+
if (!inPort || !outMain) continue
|
|
568
|
+
const targets = outbound.get(outMain.id) ?? []
|
|
569
|
+
if (
|
|
570
|
+
targets.length > 0 &&
|
|
571
|
+
targets.every((id) => flexibleByPortId.get(id) === true)
|
|
572
|
+
) {
|
|
573
|
+
flexible.add(inPort.id)
|
|
574
|
+
flexible.add(outMain.id)
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return flexible
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 接続の型が受け入れられるか:受け側が flexible なら常に可、それ以外は 伝播型 ⊆ 宣言。
|
|
581
|
+
// sdk の保存契約・backend の保存時チェック・エディタの表示はすべてこれを使う
|
|
582
|
+
// (判定が散ると表示と保存がズレる)
|
|
583
|
+
export const connectionAccepted = (
|
|
584
|
+
from: JsonSchema,
|
|
585
|
+
to: Pick<GraphPort, "schema" | "flexible">,
|
|
586
|
+
): boolean => to.flexible === true || isSchemaSubset(from, to.schema)
|
|
587
|
+
|
|
538
588
|
export const propagateDerivedPorts = <N extends PropagationNode>(
|
|
539
589
|
nodes: N[],
|
|
540
590
|
connections: PortLink[],
|
|
541
591
|
options?: PropagationOptions<N>,
|
|
542
592
|
): N[] => {
|
|
543
593
|
const resolved = resolvePortSchemas(nodes, connections, options)
|
|
594
|
+
const flexiblePorts = resolveFlexiblePorts(nodes, connections)
|
|
544
595
|
return nodes.map((node) => {
|
|
545
596
|
let touched = false
|
|
546
597
|
const ports = node.ports.map((port) => {
|
|
547
598
|
const schema = resolved.get(port.id) ?? port.schema
|
|
548
|
-
|
|
599
|
+
// start の flexible は導出結果で置き換える(false は省略=キー無し)
|
|
600
|
+
const flexible =
|
|
601
|
+
node.kind === "start"
|
|
602
|
+
? flexiblePorts.has(port.id) || undefined
|
|
603
|
+
: port.flexible
|
|
604
|
+
if (
|
|
605
|
+
JSON.stringify(schema) === JSON.stringify(port.schema) &&
|
|
606
|
+
flexible === port.flexible
|
|
607
|
+
)
|
|
608
|
+
return port
|
|
549
609
|
touched = true
|
|
550
|
-
|
|
610
|
+
const { flexible: _dropped, ...rest } = port
|
|
611
|
+
return {
|
|
612
|
+
...rest,
|
|
613
|
+
schema,
|
|
614
|
+
...(flexible ? { flexible } : {}),
|
|
615
|
+
} as typeof port
|
|
551
616
|
})
|
|
552
617
|
return touched ? { ...node, ports } : node
|
|
553
618
|
})
|
|
@@ -49,6 +49,7 @@ export declare const agentOfModel: (model: AiModel) => AiAgentId;
|
|
|
49
49
|
export declare const aiInputDeclSchema: z.ZodObject<{
|
|
50
50
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
51
51
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
52
|
+
flexible: z.ZodOptional<z.ZodBoolean>;
|
|
52
53
|
}, z.core.$strip>;
|
|
53
54
|
export type AiInputDecl = z.infer<typeof aiInputDeclSchema>;
|
|
54
55
|
export declare const aiManifestSchema: z.ZodObject<{
|
|
@@ -87,9 +88,11 @@ export declare const aiManifestSchema: z.ZodObject<{
|
|
|
87
88
|
inputs: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
88
89
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
89
90
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
91
|
+
flexible: z.ZodOptional<z.ZodBoolean>;
|
|
90
92
|
}, z.core.$strip>, z.ZodPipe<z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodTransform<{
|
|
91
93
|
schema: Record<string, unknown>;
|
|
92
94
|
resume?: boolean | undefined;
|
|
95
|
+
flexible?: boolean | undefined;
|
|
93
96
|
}, Record<string, unknown>>>]>>;
|
|
94
97
|
outputs: z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
95
98
|
}, z.core.$strip>;
|
|
@@ -65,6 +65,8 @@ export const agentOfModel = (model) => model.startsWith("claude-") ? "claude" :
|
|
|
65
65
|
export const aiInputDeclSchema = z.object({
|
|
66
66
|
schema: jsonSchemaSchema,
|
|
67
67
|
resume: z.boolean().optional(),
|
|
68
|
+
// 推奨型を維持して任意の型を受ける(接続で上書きされず、適合チェックもしない)
|
|
69
|
+
flexible: z.boolean().optional(),
|
|
68
70
|
});
|
|
69
71
|
// 旧形式(値が素の JSON Schema)は { schema: 値 } として読む。判別は「schema キーを
|
|
70
72
|
// 持つか」:JSON Schema のトップレベルに "schema" キーは現れない(メタスキーマ参照は
|
package/dist/_schemas/graph.d.ts
CHANGED
|
@@ -152,6 +152,7 @@ export declare const graphNodeSchema: z.ZodObject<{
|
|
|
152
152
|
name: z.ZodString;
|
|
153
153
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
154
154
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
155
|
+
flexible: z.ZodOptional<z.ZodBoolean>;
|
|
155
156
|
side: z.ZodOptional<z.ZodEnum<{
|
|
156
157
|
top: "top";
|
|
157
158
|
right: "right";
|
|
@@ -252,6 +253,7 @@ export declare const saveGraphBodySchema: z.ZodObject<{
|
|
|
252
253
|
name: z.ZodString;
|
|
253
254
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
254
255
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
256
|
+
flexible: z.ZodOptional<z.ZodBoolean>;
|
|
255
257
|
side: z.ZodOptional<z.ZodEnum<{
|
|
256
258
|
top: "top";
|
|
257
259
|
right: "right";
|
package/dist/_schemas/graph.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { graphPortSchema, jsonSchemaSchema, nodeKindSchema, portKeySchema, } from "./node.js";
|
|
3
|
-
import {
|
|
3
|
+
import { connectionAccepted, hasInboundConflict, kindPortsViolation, resolvePortSchemas, UNDECLARED_SCHEMA, } from "./port-spec.js";
|
|
4
4
|
// refs=ループ変数の袋(key→任意の JSON 値。上流出力の参照(refs).md)。
|
|
5
5
|
// run 開始時に定義から複製され、全 step に全量差し込まれる。
|
|
6
6
|
// key はポート key と同じ規則(^[a-z][a-z0-9_]*$)。実行系ボディ
|
|
@@ -473,9 +473,10 @@ export const saveGraphBodySchema = z
|
|
|
473
473
|
// 接続鏡映する口(program / ai の in)とコピー宣言のビューは接続元の型を採用する
|
|
474
474
|
// ので自明に通り、実質の検証対象は宣言で固定された口
|
|
475
475
|
// (component の契約・宣言のあるビューの human.in)になる
|
|
476
|
+
// flexible な in(AI の宣言 in・契約で焼き込まれた component の in)は型を問わない
|
|
476
477
|
const fromSchema = resolved.get(conn.from_port_id) ?? from.schema;
|
|
477
478
|
const toSchema = resolved.get(conn.to_port_id) ?? to.schema;
|
|
478
|
-
if (!
|
|
479
|
+
if (!connectionAccepted(fromSchema, { ...to, schema: toSchema })) {
|
|
479
480
|
ctx.addIssue({
|
|
480
481
|
code: "custom",
|
|
481
482
|
path: ["connections", i, "to_port_id"],
|
package/dist/_schemas/node.d.ts
CHANGED
|
@@ -39,6 +39,7 @@ export declare const graphPortSchema: z.ZodObject<{
|
|
|
39
39
|
name: z.ZodString;
|
|
40
40
|
schema: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
41
41
|
resume: z.ZodOptional<z.ZodBoolean>;
|
|
42
|
+
flexible: z.ZodOptional<z.ZodBoolean>;
|
|
42
43
|
side: z.ZodOptional<z.ZodEnum<{
|
|
43
44
|
top: "top";
|
|
44
45
|
right: "right";
|
package/dist/_schemas/node.js
CHANGED
|
@@ -43,6 +43,10 @@ export const graphPortSchema = z
|
|
|
43
43
|
// 再開する(AI ノードで意味を持つ。key ではなくこの属性が再開を決める。
|
|
44
44
|
// ポート型固定の撤去とセッション再開のポート属性化.md)
|
|
45
45
|
resume: z.boolean().optional(),
|
|
46
|
+
// in ポートの型属性:true なら宣言(推奨型)を維持し、接続元の型による上書き・
|
|
47
|
+
// 適合チェックをしない(AI の in で設定。start / component へは導出で流れる。
|
|
48
|
+
// AIノード入力のflexible属性(任意の型で呼べるループ).md)
|
|
49
|
+
flexible: z.boolean().optional(),
|
|
46
50
|
side: portSideSchema.optional(),
|
|
47
51
|
offset: z.number().min(0).max(1).optional(),
|
|
48
52
|
})
|
|
@@ -70,6 +70,8 @@ export type PropagationOptions<N> = {
|
|
|
70
70
|
} | null | undefined;
|
|
71
71
|
};
|
|
72
72
|
export declare const resolvePortSchemas: <N extends PropagationNode>(nodes: N[], connections: PortLink[], options?: PropagationOptions<N>) => Map<string, JsonSchema>;
|
|
73
|
+
export declare const resolveFlexiblePorts: (nodes: PropagationNode[], connections: PortLink[]) => Set<string>;
|
|
74
|
+
export declare const connectionAccepted: (from: JsonSchema, to: Pick<GraphPort, "schema" | "flexible">) => boolean;
|
|
73
75
|
export declare const propagateDerivedPorts: <N extends PropagationNode>(nodes: N[], connections: PortLink[], options?: PropagationOptions<N>) => N[];
|
|
74
76
|
export type SchemaFit = "ok" | "mismatch" | "unknown";
|
|
75
77
|
export declare const schemaFit: (sub: JsonSchema, sup: JsonSchema) => SchemaFit;
|
|
@@ -382,6 +382,9 @@ export const resolvePortSchemas = (nodes, connections, options) => {
|
|
|
382
382
|
for (const port of node.ports) {
|
|
383
383
|
if (port.io !== "in")
|
|
384
384
|
continue;
|
|
385
|
+
// flexible な in は宣言(推奨型)を維持する(接続の型を問わない口)
|
|
386
|
+
if (port.flexible === true)
|
|
387
|
+
continue;
|
|
385
388
|
const sources = inbound.get(port.id) ?? [];
|
|
386
389
|
const inSchema = mergeInboundSchemas(sources.map((id) => resolved.get(id) ?? UNDECLARED_SCHEMA));
|
|
387
390
|
if (!isUndeclaredSchema(inSchema))
|
|
@@ -426,16 +429,68 @@ export const resolvePortSchemas = (nodes, connections, options) => {
|
|
|
426
429
|
};
|
|
427
430
|
// ノード配列へ伝播結果を書き戻す(接続変更時のポートデータ自動更新)。
|
|
428
431
|
// schema が変わらないポート・ノードは同じ参照を返す
|
|
432
|
+
// flexible の導出:start の in / out main は、接続先の in が1つ以上あり**全て** flexible
|
|
433
|
+
// (AI の宣言 in・契約で焼き込まれた component の in)のときだけ flexible。
|
|
434
|
+
// 非 flexible の接続先(program 等)が1つでも混ざれば型どおりの入力が要るので false。
|
|
435
|
+
// 直結のみ(wait 等の素通し経由は型と同じく流さない)。戻りは flexible になる port_id の集合
|
|
436
|
+
export const resolveFlexiblePorts = (nodes, connections) => {
|
|
437
|
+
const flexibleByPortId = new Map();
|
|
438
|
+
for (const node of nodes) {
|
|
439
|
+
if (node.kind === "start")
|
|
440
|
+
continue;
|
|
441
|
+
for (const port of node.ports) {
|
|
442
|
+
if (port.io === "in")
|
|
443
|
+
flexibleByPortId.set(port.id, port.flexible === true);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const outbound = new Map();
|
|
447
|
+
for (const conn of connections) {
|
|
448
|
+
const outs = outbound.get(conn.from_port_id) ?? [];
|
|
449
|
+
outs.push(conn.to_port_id);
|
|
450
|
+
outbound.set(conn.from_port_id, outs);
|
|
451
|
+
}
|
|
452
|
+
const flexible = new Set();
|
|
453
|
+
for (const node of nodes) {
|
|
454
|
+
if (node.kind !== "start")
|
|
455
|
+
continue;
|
|
456
|
+
const inPort = node.ports.find((p) => p.io === "in");
|
|
457
|
+
const outMain = node.ports.find((p) => p.io === "out" && p.key === "main");
|
|
458
|
+
if (!inPort || !outMain)
|
|
459
|
+
continue;
|
|
460
|
+
const targets = outbound.get(outMain.id) ?? [];
|
|
461
|
+
if (targets.length > 0 &&
|
|
462
|
+
targets.every((id) => flexibleByPortId.get(id) === true)) {
|
|
463
|
+
flexible.add(inPort.id);
|
|
464
|
+
flexible.add(outMain.id);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return flexible;
|
|
468
|
+
};
|
|
469
|
+
// 接続の型が受け入れられるか:受け側が flexible なら常に可、それ以外は 伝播型 ⊆ 宣言。
|
|
470
|
+
// sdk の保存契約・backend の保存時チェック・エディタの表示はすべてこれを使う
|
|
471
|
+
// (判定が散ると表示と保存がズレる)
|
|
472
|
+
export const connectionAccepted = (from, to) => to.flexible === true || isSchemaSubset(from, to.schema);
|
|
429
473
|
export const propagateDerivedPorts = (nodes, connections, options) => {
|
|
430
474
|
const resolved = resolvePortSchemas(nodes, connections, options);
|
|
475
|
+
const flexiblePorts = resolveFlexiblePorts(nodes, connections);
|
|
431
476
|
return nodes.map((node) => {
|
|
432
477
|
let touched = false;
|
|
433
478
|
const ports = node.ports.map((port) => {
|
|
434
479
|
const schema = resolved.get(port.id) ?? port.schema;
|
|
435
|
-
|
|
480
|
+
// start の flexible は導出結果で置き換える(false は省略=キー無し)
|
|
481
|
+
const flexible = node.kind === "start"
|
|
482
|
+
? flexiblePorts.has(port.id) || undefined
|
|
483
|
+
: port.flexible;
|
|
484
|
+
if (JSON.stringify(schema) === JSON.stringify(port.schema) &&
|
|
485
|
+
flexible === port.flexible)
|
|
436
486
|
return port;
|
|
437
487
|
touched = true;
|
|
438
|
-
|
|
488
|
+
const { flexible: _dropped, ...rest } = port;
|
|
489
|
+
return {
|
|
490
|
+
...rest,
|
|
491
|
+
schema,
|
|
492
|
+
...(flexible ? { flexible } : {}),
|
|
493
|
+
};
|
|
439
494
|
});
|
|
440
495
|
return touched ? { ...node, ports } : node;
|
|
441
496
|
});
|
package/docs/development.md
CHANGED
|
@@ -56,10 +56,12 @@ views/
|
|
|
56
56
|
走り続ける)。省略時は `claude-opus`。認証は repo の Actions secrets に置く:Claude Code は
|
|
57
57
|
`ANTHROPIC_API_KEY`(または `CLAUDE_CODE_OAUTH_TOKEN`)、Codex は明示的な `auth_mode` と対応する認証情報(下記「Codex の認証方式」参照)
|
|
58
58
|
(`env` に宣言しなくても渡される)
|
|
59
|
-
- `inputs` は1件以上・key 自由。値は `{ "schema": <JSON Schema>, "resume": true }` の形
|
|
59
|
+
- `inputs` は1件以上・key 自由。値は `{ "schema": <JSON Schema>, "resume": true, "flexible": true }` の形
|
|
60
60
|
(`resume` は省略可。true の口から入力が届いた実行は、同じノードの直近セッションを
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
復元して再開する——差し戻し→修正の続き実行に使う。`flexible` も省略可。true の口は
|
|
62
|
+
`schema` を推奨型として維持し、接続で型が上書きされず型の違う接続も通る——この AI が
|
|
63
|
+
先頭のループを他のループや API から型を揃えずに呼べるようにする)。旧形式(値が素の
|
|
64
|
+
JSON Schema)も読める
|
|
63
65
|
- 実行時はラッパーがエージェント CLI(Claude Code / Codex、非対話)を起動し、`outputs` の
|
|
64
66
|
スキーマに適合する output を検証して報告する。出口が複数あれば AI が1つ選ぶ(複数出口=分岐)
|
|
65
67
|
- **文体の既定は「短く」**:出力の文章は承認画面・Slack・顧客への回答としてそのまま人が読むため、
|
package/package.json
CHANGED
|
@@ -59,7 +59,7 @@ secret)でも、**足りない値を推測・生成・空文字で代用しな
|
|
|
59
59
|
|
|
60
60
|
疎通確認のあと `GET {MAWARU_API_URL}/tenants/{MAWARU_TENANT_ID}/openapi.json`
|
|
61
61
|
(ヘッダ `x-api-key: $MAWARU_API_KEY`)を取得すると、機械 API 面
|
|
62
|
-
(loops の一覧・作成・詳細・graph PUT、runs/start、runs/{runId}
|
|
62
|
+
(loops の一覧・作成・詳細・graph PUT・versions/publish、runs/start、runs/{runId} の参照・cancel・complete、members、api-keys、steps の end / decision)の
|
|
63
63
|
パス・メソッド・リクエストボディ(Zod 由来の JSON Schema)が載っている。ここが常に正。
|
|
64
64
|
段階導入中で未掲載のエンドポイントもある(その場合だけ `api/index.ts` 相当を辿る)。
|
|
65
65
|
|
|
@@ -73,7 +73,15 @@ repo との矛盾・使用中ノードの削除は 409。**graph PUT は冪等
|
|
|
73
73
|
|
|
74
74
|
## 手順
|
|
75
75
|
|
|
76
|
-
1
|
|
76
|
+
ループは次の4段階で作る。**言われなくても必ず 1 のスケルトンから始め、段階を飛ばさない。**
|
|
77
|
+
graph の保存(`PUT loops/{loopId}/graph`)は常に下書き(draft)に入り、4 で publish するまで
|
|
78
|
+
本番の run には使われない。
|
|
79
|
+
|
|
80
|
+
### 1. スケルトンを作って合意する
|
|
81
|
+
|
|
82
|
+
ノードの配置と接続だけを下書きに保存する。中身(repo のプログラム・prompt)はまだ作らない。
|
|
83
|
+
|
|
84
|
+
1. **設計**:要件から必要なノード(program / ai / human / wait / join)と流れを決める。
|
|
77
85
|
1つの出口から複数の線を引くと枝が並行に走る(fan-out)。枝の結果を待ち合わせるときは
|
|
78
86
|
合流(`join`)を置く:線のつながった入口が全部揃うまで待ち、`{ [入口の key]: 値 }` にまとめて
|
|
79
87
|
`main` から流す。並行した枝を合流させずに同じノード(待機など)へ戻すと、枝の数だけ走り続ける。
|
|
@@ -83,26 +91,56 @@ repo との矛盾・使用中ノードの削除は 409。**graph PUT は冪等
|
|
|
83
91
|
(`component.component_loop_id`=作った for)を置く。配置ノードの in / out は中身の型の配列に
|
|
84
92
|
自動で焼き込まれ、全件そろうと入力と同じ順の配列で `main` から流れる(1件ずつは同時に走る)。
|
|
85
93
|
中身の出口(end につないだ in)は1つにする。
|
|
86
|
-
2.
|
|
87
|
-
|
|
88
|
-
**AI ノードの決定論的な処理は prompt ではなく `setup.sh` に書く**(下記)。
|
|
89
|
-
**作った・変えたものは GitHub へ push してから次へ進む**(mawaru は repo を GitHub 経由で
|
|
90
|
-
読むため、未 push だと graph 保存の検証・書き戻しと食い違って 409 になる)。
|
|
91
|
-
3. **ループ作成**:`POST loops`。
|
|
92
|
-
4. **graph ボディを組む**:
|
|
94
|
+
2. **ループ作成**:`POST loops`(既存ループの更新なら不要)。
|
|
95
|
+
3. **graph ボディを組む**(中身は空のまま):
|
|
93
96
|
- node / port の `id` はクライアント採番(`uuidgen` などで UUID を作る)
|
|
94
|
-
- program / ai
|
|
95
|
-
|
|
96
|
-
- kind
|
|
97
|
-
|
|
97
|
+
- program / ai ノードには `program` / `ai`(dir の指定)を付けない。dir 未設定のノードは
|
|
98
|
+
repo へ書き戻されないので、repo 側に何も無くても保存できる
|
|
99
|
+
- ポートは kind 別の固定構成(`port-spec.ts` の `kindPortsViolation`)と分岐する出口の key だけ
|
|
100
|
+
決め、`schema` は `{}`(未宣言)でよい。型は 2 で config.json と一緒に決める。
|
|
101
|
+
program の in は `in`。join は in を枝の数だけ(key 自由。例 `in_1`, `in_2`)+ out `main`
|
|
102
|
+
- human は担当(`human.assignment.units`)が保存に必須なので、誰が承認するかもここで決める
|
|
98
103
|
- ループ変数(refs=run 全体で読める key→値 の袋)が要るならトップレベルの `refs` に入れる。
|
|
99
104
|
ノード単位の宣言やバインドは無い。handler は実行時に `refs.json` で全量を受け取り、
|
|
100
105
|
`end` の `refs` パッチで値を更新できる(config.json の `refs` はループ定義からの転写=生成物)。
|
|
101
106
|
キー `input` は予約(run の起動入力が自動で入る。定義に置くと 422)
|
|
102
107
|
- `position_x` / `position_y` は左→右に流れる単純配置(例: x を 320 刻み、y は 240)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
4. **保存**:`PUT loops/{loopId}/graph`。400 / 422 / 409 ならメッセージを読んで直して再送。
|
|
109
|
+
5. **合意**:エディタ URL `{MAWARU_APP_URL}/loops/{loopId}/edit` をユーザーに渡し、ノードの並びと
|
|
110
|
+
つながりを見てもらう。**OK をもらうまで 2 へ進まない**(直しの依頼は graph を直して保存し、
|
|
111
|
+
もう一度見てもらう)。
|
|
112
|
+
|
|
113
|
+
### 2. 各ノードの中身を実装し、1つずつ確かめる
|
|
114
|
+
|
|
115
|
+
上流のノードから順に、1つずつ次を繰り返す:
|
|
116
|
+
|
|
117
|
+
1. **repo 側の実装**:`nodes/program/<dir>/` / `nodes/ai/<dir>/` を作る
|
|
118
|
+
(config.json → `npx mawaru typegen` → main.ts 実装 → `npx mawaru validate`)。
|
|
119
|
+
config.json の inputs / outputs はスケルトンのポートの key に合わせ、ここで型を決める。
|
|
120
|
+
**AI ノードの決定論的な処理は prompt ではなく `setup.sh` に書く**(下記)。
|
|
121
|
+
**GitHub へ push してから次へ進む**(mawaru は repo を GitHub 経由で読むため、未 push だと
|
|
122
|
+
graph 保存の書き戻しと食い違って 409 になり、テスト実行も古い中身で走る)。
|
|
123
|
+
2. **ノードに結びつける**:graph の該当ノードに `program.program_dir` / `ai.ai_dir` を設定し、
|
|
124
|
+
ports を config.json の inputs / outputs の写しにして保存する(in ポート key は `in`、
|
|
125
|
+
out ポートは outputs の各 key)。human は承認ビュー、wait は待ち方もここで設定する。
|
|
126
|
+
3. **単体で確かめる**:そのノードだけをテスト実行する。`POST runs/start` に `kind: "test"`・
|
|
127
|
+
`test_scope: "only"`・`start_node_id`(そのノード)・`payload`(サンプル入力。上流を確かめ済みなら
|
|
128
|
+
その output を使う)・`start_by`(ユーザーの user_id。`GET members` で引く。API キー起動では必須)を
|
|
129
|
+
渡し、`GET runs/{runId}` で step の status と output を見る。下書きの program / ai は
|
|
130
|
+
ブランチ先頭の commit で走る。失敗したら直して push し、もう一度実行する。
|
|
131
|
+
human はテスト実行の担当が起動者になるので、承認・差し戻しはユーザーに操作してもらう。
|
|
132
|
+
|
|
133
|
+
### 3. 全体を通して確かめる
|
|
134
|
+
|
|
135
|
+
開始ノード(kind `start`)を `start_node_id` に、`test_scope: "from"` でテスト実行し、end まで
|
|
136
|
+
流れることを確かめる(分岐があれば主な道を一通り)。human を通る道はユーザーに操作してもらう。
|
|
137
|
+
結果をユーザーに報告する。
|
|
138
|
+
|
|
139
|
+
### 4. publish して起動する
|
|
140
|
+
|
|
141
|
+
ユーザーの OK をもらってから `POST loops/{loopId}/versions/publish` で下書きを公開する
|
|
142
|
+
(AI / Program の実行 commit はここで固定される)。続けて `POST runs/start`(`loop_id`・`payload`。
|
|
143
|
+
`kind` を省くと本番 run)で起動し、run の URL `{MAWARU_APP_URL}/runs/{runId}` をユーザーに渡す。
|
|
106
144
|
|
|
107
145
|
## AI ノード:出力は短く(既定)
|
|
108
146
|
|